ArrayList底層源碼分析

聲明:本文爲作者原創,請勿裝載,如過轉載,請註明轉載地址

ArrayList底層源碼分析

(1) ArrayList是基於數組實現的,是一個動態數組,其容量能自動增長,不像數組一旦初始化長度就不可被改變

(2) ArrayList不是線程安全的,只能用在單線程環境下,多線程環境下可以考慮用Collections.synchronizedList(List l)函數返回一個線程安全的ArrayList類,也可以使用concurrent併發包下的CopyOnWriteArrayList類。

(3) ArrayList實現了Serializable接口,因此它支持序列化,能夠通過序列化傳輸,實現了RandomAccess接口,支持快速隨機訪問,實際上就是通過下標序號進行快速訪問,實現了Cloneable接口,能被克隆。

增刪慢:每次刪除元素,都需要更改數組的長度,拷貝以及移動元素的位置

查詢快:由於數組在內存中是一塊連續的空間,因此可以根據索引快速獲取某個位置上的元素。

1. 繼承Serializable接口

介紹:類的序列化由java.io.Serializable接口的類啓用,不實現此接口的類將不會使用任何狀態序列化或反序列化。可序列化類的所有子類型都是可序列化的。序列化接口沒有方法和字段,僅用於標識可序列化的語義。

序列化:將對象的數據寫入到文件中

反序列化:將文件中對象的數據讀取出來

//Serializable 源碼,接口中並沒有方法
public interface Serializable {
}

2. 繼承Cloneable接口

一個類實現Cloneable接口來指示Object.clone()方法,該方法對於該類的實例進行字段的複製是合法的,如果不實現該接口,那麼調用clone()方法戶拋出異常。克隆就是根據已有的數據創造一份新的完全一樣的數據拷貝。

// Cloneable 源碼,接口中並沒有方法
public interface Cloneable {
}

克隆的前提:被克隆對象所在的類必須實現Cloneable接口,必須重寫clone()方法。

public class CloneDemo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");

        //調用方法進行克隆
        Object obj = list.clone();
        System.out.println(obj==list); //false
        System.out.println(obj);       //[aaa, bbb, ccc]
        System.out.println(list);      //[aaa, bbb, ccc]

    }
}

2.1 淺拷貝

//Student類
@Data
public class Student implements Cloneable {
    private String name;
    private int age;
    private Skill skill;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
	//省略構造函數、toString()方法
    
}

//Skill類
@Data
public class Skill {
    private String skillName;
	//省略構造函數、toString()方法
}

//CloneDemo1測試類
public class CloneDemo1 {
    public static void main(String[] args) throws CloneNotSupportedException {
        ArrayList<String> list = new ArrayList<>();
        Skill skill = new Skill("彈鋼琴");
        Student stu1 = new Student("小明",18,skill);

        //調用clone()方法阿進行數據的拷貝
        Object stu2 = stu1.clone();

        System.out.println(stu1);
        System.out.println(stu2);

        //修改stu1的年齡(修改基本類型的值)
        stu1.setAge(30);
        //修改技能(修改引用類型的值)
        skill.setSkillName("畫畫");

        System.out.println(stu1);
        System.out.println(stu2);
    }
}

結果:

Student{name='小明', age=18, skill=Skill{skillName='彈鋼琴'}}
Student{name='小明', age=18, skill=Skill{skillName='彈鋼琴'}}
Student{name='小明', age=30, skill=Skill{skillName='畫畫'}}
Student{name='小明', age=18, skill=Skill{skillName='畫畫'}}

存在的問題:基本數據類型可以達到完全複製,但是引用數據類型不可以

原因:在學生對象stu1被拷貝時,其屬性skill(引用數據類型)僅僅是拷貝了一份引用,因此當被克隆對象stu1的skill的值發生改變時,克隆對象stu2的屬性skill也將跟着改變。

如果屬性是基本類型,拷貝的就是基本類型的值;如果屬性是內存地址(引用類型),拷貝的就是內存地址。

(1) 對於基本數據類型的成員對象,因爲基礎數據類型是值傳遞的,所以是直接將屬性值賦值給新的對象。基礎類型的拷貝,其中一個對象修改該值,不會影響另外一個。
(2) 對於引用類型,比如數組或者類對象,因爲引用類型是引用傳遞,所以淺拷貝只是把內存地址賦值給了成員變量,它們指向了同一內存空間。改變其中一個,會對另外一個也產生影響。

2.2 深拷貝

//Skill類,實現Cloneable接口
@Data
public class Skill implements Cloneable {
    private String skillName;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

//Student類
@Data
public class Student implements Cloneable {
    private String name;
    private int age;
    private Skill skill;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //深拷貝不能簡單的調用父類的方法
        //先克隆一個學生對象
        Student stu = (Student)super.clone();
        //再克隆一個skill對象
        Skill skill = (Skill) stu.skill.clone();
        //將克隆出來的skill對象賦值給stu對象的成員變量
        stu.setSkill(skill);
        return stu;
    }
	//省略...
}

//測試類不變

結果:

Student{name='小明', age=18, skill=Skill{skillName='彈鋼琴'}}
Student{name='小明', age=18, skill=Skill{skillName='彈鋼琴'}}
Student{name='小明', age=30, skill=Skill{skillName='畫畫'}}
Student{name='小明', age=18, skill=Skill{skillName='彈鋼琴'}}

深拷貝,在拷貝引用類型成員變量時,爲引用類型的數據成員另闢了一個獨立的內存空間,實現真正內容上的拷貝。

  1. 對於基本數據類型的成員對象,因爲基礎數據類型是值傳遞的,所以是直接將屬性值賦值給新的對象。基礎類型的拷貝,其中一個對象修改該值,不會影響另外一個(和淺拷貝一樣)。
    (2) 對於引用類型,比如數組或者類對象,深拷貝會新建一個對象空間,然後拷貝里面的內容,所以它們指向了不同的內存空間。改變其中一個,不會對另外一個也產生影響。
    (3) 對於有多層對象的,每個對象都需要實現 Cloneable 並重寫 clone() 方法,進而實現了對象的串行層層拷貝。
    (4) 深拷貝相比於淺拷貝速度較慢並且花銷較大。

深拷貝後,不管是基礎數據類型還是引用類型的成員變量,修改其值都不會相互造成影響。

3. RandomAccess接口

RandomAccess接口這個空架子的存在,是爲了能夠更好地判斷集合是否ArrayList或者LinkedList,從而能夠更好選擇更優的遍歷方式,提高性能!

4. ArrayList源碼分析

4.1 數據域

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
 
   	//數組使用空參數構造函數初始化時,第一次添加元素後的初始化容量
    private static final int DEFAULT_CAPACITY = 10;

    //一個空數組,用於帶參構造函數的初始化
    private static final Object[] EMPTY_ELEMENTDATA = {};

    //一個空數組,用於空參構造函數初始化
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //當前數據對象存放地方
    transient Object[] elementData; // non-private to simplify nested class access

   	//當前數組中元素的個數
    private int size;
    
    //數組最大可分配的容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    // 集合數組修改次數的標識(由AbstractList繼承下來)(fail-fast機制)
    protected transient int modCount = 0;
}

4.2 構造函數

/**
 * 1、空參數構造函數
 */
public ArrayList() {
    //讓的elelmentData指向一個空數組,這個數組的容量爲0,第一次調用add()方法時會初始化爲10
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
  * 2、創建指定長度的構造函數
  */
public ArrayList(int initialCapacity) {
    //如果傳入的容量的大於0,就創建一個指定容量的空數組用來存儲元素
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
        
    //創建一個長度爲0的空數組,這個數組的容量爲0,第一次調用add()方法時會初始化爲1
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    }
}

/**
 * 3、構造一個包含指定元素的列表的列表list,按照集合的順序返回迭代器。
 */
public ArrayList(Collection<? extends E> c) {
    //將傳入的集合轉化爲數組並淺拷貝給elementData
    elementData = c.toArray();
    
    //將轉換後的數組長度賦值給size,並判斷是否爲0
    if ((size = elementData.length) != 0) {
        //數組的創建和拷貝
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 用EMPTY_ELEMENTDATA替換空數組,在add()時,容量就會變成1
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

**jdk8:**ArrayList中維護了Object[] elementData,初始容量爲0,第一次添加時,將初始elementData的容量爲10再次添加時,如果容量足夠,則不用擴容直接將新元素賦值到第一個空位上,如果容量不夠,會擴容1.5倍

**jdk7:**ArrayList中維護了Object[] elementData,初始容量爲10.添加時,如果容量足夠,則不用擴容直接將新元素賦值到第一個空位上。如果容量不夠,會擴容1.5倍

jdk7 相當於餓漢式,創建對象時,則初始容量爲10
jdk8 相當於懶漢式,創建對象時,並沒有初始容量爲10,而在添加時纔去初始容量爲10

4.3 在數組末尾添加一個元素

4.3.1 add(E e) 向數組末尾添加一個元素

在這裏插入圖片描述

// 在數組的結尾添加一個元素
public boolean add(E e) {
    // 確保數組已使用長度(size)加1之後足夠存下 下一個數據
    ensureCapacityInternal(size + 1);   
    
    // 數組的下一個位置存放傳入元素,同時將size後移
    elementData[size++] = e;
    
    //始終返回true
    return true;
}

4.3.2 ensureCapacityInternal(int minCapacity)保證存在剩餘空間存放要添加的元素

/**
	1、如果調用的是空參數構造函數:比較初始容量和所需最小容量的大小,並返回較大的
	2、如果調用的帶參數構造函數:直接返回最小容量(size+1)
**/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    /**
    	如果ArrayList創建對象時調用的是空參數構造函數:
    		第一次調用add()方法添加元素時:會在這兒將數組的容量初始化爲10
    		隨後調用add()方法添加元素時:會比較添加該元素所需的最小容量minCapacity和10的大小,返回大的	 */
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
	/**
		如果ArrayList創建對象時調用的爲帶參構造函數:直接返回最小容量minCapacity = size+1
	*/
    return minCapacity;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    //如果 minCapacity(即size+1當前需要的最小容量) > elementData.length(數組的容量),需要擴容
    //使得存在剩餘的數內存存放要添加的元素
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

4.3.3 grow(int minCapacity)擴容機制

ArrayList的擴容機制:

private void grow(int minCapacity) {
    // 獲取數組容量
    int oldCapacity = elementData.length;
    
    // 擴容1.5倍(將數組的容量擴容1.5倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 如果擴展爲1.5倍後還不滿足需求,則直接擴展爲需求值
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    // 檢查擴容後的容量是否大於最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    // 申請一塊更大內存容量的數組,將原來數組中的元素挪到這個新數組中,同時將elementData指向這個新數組
    // 原來的舊的數組由於沒有引用變量指向他,就會被垃圾回收機制回收掉
    elementData = Arrays.copyOf(elementData, newCapacity);
}

總結:在數組的末尾添加一個元素:

(1) 調用ensureCapacityInternal(size + 1);方法,該方法的作用是確保數組的長度加1後內存是充足的,就是說保證能夠數組還能存放一個元素。

在這裏插入圖片描述

(2) 調用calculateCapacity(elementData, minCapacity)計算添加一個元素所需的最小容量minCapacity;根據ArrayList創建對象時使用的構造方法分爲兩種情況:

如果使用空參數構造函數創建ArrayList對象,會在第一次調用add()方法添加元素時,將數組的容量初始化爲10,隨後每次調用add()方法添加元素時都會比較初始容量10與minCapacity的大小,並返回大的容量。

如果使用帶參構造函數創建ArrayList對象,會直接但是minCapacity。

(3) 調用ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));該方法就是ensureCapacityInternal(size + 1)方法的底層,其作用是確保數組的長度加1後內存是充足的,就是說保證能夠數組還能存放一個元素。

若添加一個元素所需的最小容量minCapacity大於數組的長度if (minCapacity - elementData.length > 0)那麼就需要擴容,此時就會調用擴容方法:grow(int minCapacity)

(4) 調用 grow(int minCapacity)方法,該方法會對數組容量進行擴容。

在這裏插入圖片描述

在擴容時會申請一塊更大內存容量的數組,將原來數組中的元素拷貝到這個新的數組中,同時讓elementData指向該數組,而原來的數組由於沒有新的指針指向它,就會被垃圾回收機制回收。

在擴容時,首先考慮擴容1.5倍(如果擴容的太大,可能會浪費更多的內存,如果擴容的太小,又會導致頻繁擴容,申請數組拷貝元素等對添加元素的性能消耗較大,因此1.5倍剛好)。如果擴容1.5倍還是內存不足,就會直接擴容到添加元素所需的最小的容量。同時不能超過數組的最大容量Integer.MAX_VALUE - 8

4.4 add(int index, E element)方法

先數組的指定位置添加元素:

public void add(int index, E element) {
    //判斷索引是否越界
    rangeCheckForAdd(index);

    //確保數組size+1之後能夠存下 下一個數據
    ensureCapacityInternal(size + 1); 
    
    /**
    	源數組 elementData 從傳入形參index處開始複製,複製size-index個元素
    	目標數組 elementData 從index+1處開始粘貼,粘貼從源數組賦值的元素
    **/
    System.arraycopy(elementData, index, elementData, index + 1,size - index);
    
    //把index處的元素替換成新的元素。
    elementData[index] = element;
    
    //將size後移
    size++;
}

//判斷索引是否越界
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

// 這是一個本地方法,由C語言實現。
public static native void arraycopy(Object src,  // 源數組
									int  srcPos, // 源數組要複製的起始位置
                                    Object dest, // 目標數組(將原數組複製到目標數組)
                                    int destPos, // 目標數組起始位置
                                    int length   // 複製源數組的長度
                                    );

總結:在數組的指定位置添加元素

(1) 調用rangeCheckForAdd(index)方法判斷輸入的索引是否越界

(2) 調用ensureCapacityInternal(size + 1)方法確保數組的容量充足,至少保證能再裝下一個元素。

(3) 調用System.arraycopy(elementData, index, elementData, index + 1,size - index);方法,這是C語言的一個實現方法,這個方法的作用是複製elementData數組中(index,size-index)區間內的元素,粘貼到elementData數組中(index+1,size-index+1)的位置處。

在這裏插入圖片描述

從圖中可看出,表現出的結果就是將elementData數組中的元素,從要插入的位置index開始向後移動一個位置,給要插入的元素騰出一個位置,將該元素插入到index位置處。

4.5 set(int index, E element) 方法

設置index位置處的元素

public E set(int index, E element) {
    rangeCheck(index);
	
    //記錄index位置處的舊值
    E oldValue = elementData(index);
    //將數組index索引處的元素設置爲新值element
    elementData[index] = element;
    //返回舊值
    return oldValue;
}

//判斷索引越界
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

4.6 get(int index)

獲取指定索引位置處的元素

public E get(int index) {
    //判斷索引越界
    rangeCheck(index);
	//返回指定索引位置處的元素
    return elementData(index);
}

因爲數組的內存是連續的,因此可以根據索引直接獲取元素。

4.7 remove(int index)

//刪除該列表中指定位置的元素,將所有後續元素轉移到前邊(將其中一個元素從它們中減去指數)
public E remove(int index) {
    //判斷索引越界
    rangeCheck(index);

    modCount++;
    //獲取index位置處的舊值
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        /**
            源數組 elementData 從傳入形參index+1處開始複製,複製size-index-1個元素
            目標數組 elementData 從index處開始粘貼,粘貼從源數組賦值的元素
        */
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    //將size減1指向最後一個元素位置,再將最後一個位置清空
    elementData[--size] = null; 

    return oldValue;
}

// 這是一個本地方法,由C語言實現。
public static native void arraycopy(Object src,  // 源數組
									int  srcPos, // 源數組要複製的起始位置
                                    Object dest, // 目標數組(將原數組複製到目標數組)
                                    int destPos, // 目標數組起始位置
                                    int length   // 複製源數組的長度
                                    );

總結:

(1) 調用rangeCheckForAdd(index)方法判斷輸入的索引是否越界

(3) 調用System.arraycopy(elementData, index+1, elementData, index, size - index - 1);該方法的作用是複製elementData數組中(index+1,size-index-1)區間內的元素,粘貼到elementData數組中的(index,size-index-2)位置處。

在這裏插入圖片描述
從圖中可看出,表現出的結果就是將elementData數組中的元素,從要刪除的元素的後面一個位置開始到末尾的元素結束都向前移動一個位置,最後將size–,將最後一個元素置爲null

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章