【集合框架】ArrayList源碼分析

一、前言

小胖覺得呀,Java集合框架,是面試考察的一個大重點!不管是你翻看大廠還是小公司,面試中都有對集合的要求,集合的知識也是非常多的。我在CSDN上找了很多的資料,包括框架圖、ArrayList考察點等。有很多比較好的資料,包括敖丙寫的3篇文章質量都是非常高的。本着分享技術的目的,也想自己好好的梳理一下。本系列文章包含:List、Set、Map、HashMap、CurrentHashMap、CopyOnWriteArrayList等。還是一個目的,讓你能搞懂一些集合的知識,以及集合的經典面試題。本文只有ArrayList的源碼分析

二、Java集合有哪些

http://ftp.zouzhifeng.com/collection.png
http://ftp.zouzhifeng.com/map.png
上面兩個圖是我翻看了JDK1.8源碼之後,畫的一個簡單的圖,複雜的圖連線真的很多,完全看不了。網上的圖也都非常的複雜。

簡單解釋上面類的關係:

  1. Vector、ArrayList和LinkedList都實現了List接口

  1. HashSet實現了Set接口,LinkedHashSet繼承自HashSet
  2. SortedSet繼承了Set接口,TreeSet實現NavigableSet,NavigableSet繼承SortedSet

  1. Hashtable和hashMap實現Map接口,LinkedHashMap繼承HashMap
  2. SortedMap繼承了Map接口,TreeMap實現NavigableMap,NavigableMap繼承SortedMap

很複雜,真的很複雜!!!

三、面試考察重點在哪?

ArrayList底層原理、HashMap底層原理、CurrentHashMap與HashMap的區別。這三個內容是非常重要的,基本上都是必須要掌握的知識。

四、List家族有哪些?

1.Vector

Vector集合是一個比較古老的集合類,在Java1.1的時候就有了,Vector實現List集合,元素可以重複,順序按照插入的順序(即不會自動排序)。他是線程安全的,但是不推薦使用,因爲它使用了Synchronized同步機制的對象鎖的形式。
很多的方法都是下面的這種形式

public synchronized E set(int index, E element) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
2.ArrayList底層原理

ArrayList集合:實現List集合,元素可以重複,順序按照插入的順序(即不會自動排序)。他是線程不安全的,單線程考慮使用ArrayList。

然而答上面的內容很難滿足面試官
他們經常就會問你你知道ArrayList底層嗎?

我曾經第一次遇到這個問題的時候是懵的,底層不就是數組,順序表來實現…

其實他也就是想問你,是否看過源碼。

我覺得你可以從以下幾個方面來介紹:ArrayList的3個構造參數、add方法或插入方法、刪除方法、ArrayList擴容機制等。

ArrayList構造方法

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];  //創建對應大小的數組
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;   //創建一個空的數組
    } else {
            throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);  //傳入的參數非法
    }
}
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  //默認是創建一個空數組對象
}
public ArrayList(Collection<? extends E> c) {   //把集合C的元素在創建的時候添加到數組當中
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

ArrayList的無參數構造方法,會傳入一個空的對象。一個int參數的構造方法,會傳入對象大小的對象。也可以傳入一個集合,當集合有值的時候,把對象複製到elementData中。

add方法、插入方法

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;   //在size中插入,然後size加1
    return true;
}
public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1, size - index); //元素後移,空出index位置
    elementData[index] = element;  //插入index
    size++;
}

private void rangeCheckForAdd(int index) {   //檢查index位置是否合法
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

ArrayList的add方法有兩個,直接add元素的會先檢查集合是否需要擴容,然後在末尾添加元素e,elementData[size++] = e;

第二個會檢查index的位置,防止越界。之後使用arraycopy的方法,把index之後的元素向後移以爲,再放入元素。

System.arraycopy(elementData, index, elementData, index + 1, size - index);
/**
    @param      src      the source array.     //原數組
    @param      srcPos   starting position in the source array.  //開始移動的位置
    @param      dest     the destination array.  //目標數組
    @param      destPos  starting position in the destination data.   //開始複製的地址
    @param      length   the number of array elements to be copied.   //複製的長度
*/
public static native void arraycopy(Object src,  int  srcPos,Object dest, int destPos,int length);

這個arraycopy是一個C語言寫的方法,它的5個參數:第一個是原來的數組,第二個是開始的位置,第三個是複製的目標數組,第四個是開始位置複製的地址,第五個是複製的長度。

把array數組中的元素全部複製到newArray中,應該怎麼寫?

System.arraycopy(array, 0, newArray, 0, size );

這樣是不是就理解了一些

ArrayList刪除方法

public E remove(int index) {
    rangeCheck(index);     //檢查索引的下標是否正確

    modCount++;
    E oldValue = elementData(index);
    
    int numMoved = size - index - 1;   //需要移動的元素的個數
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);   //移動元素
    elementData[--size] = null; // clear to let GC do its work    //把移動完最後一個元素置爲null

    return oldValue;
}
public boolean remove(Object o) {  //按照對象,移除某一個對象
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)  
            if (o.equals(elementData[index])) {    //移除相應的對象
                fastRemove(index);
                return true;
            }
    }
    return false;
}

ArrayList擴容機制
默認的初始化的情況下容量是10,add之後會有ensureCapacityInternal的判斷,把size+1和原來的elementData對象的Size進行對比,返回的DEFAULT_CAPACITY和minCapacity的最大值,當超過10之後,11比10大,進入到grow的函數,使用位運算,擴容原來的一半。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!   //這裏判斷是否擴容
    elementData[size++] = e;
    return true;
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {   //這裏的一層判斷真的很奇怪,如果數組中沒有元素,分配10個大小的元素,否則返回size+1
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureCapacityInternal(int minCapacity) {    //minCapacity 此時等於size+1
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); 
}

private void ensureExplicitCapacity(int minCapacity) {     //minCapacity 此時等於size+1
    modCount++;
    
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)      //minCapacity比數組中元素的長度長的時候
        grow(minCapacity);                         //做擴容的方法
}

private void grow(int minCapacity) {               //擴容方法
    // overflow-conscious code 
    int oldCapacity = elementData.length;          //原來長度       
    int newCapacity = oldCapacity + (oldCapacity >> 1);   //新的長度是原來的1.5倍,原來長度右移一位就是原來長度的0.5了
    if (newCapacity - minCapacity < 0)             //第一次插入的時候會使用,newCapacity=oldCapacity=0,所以直接等於10
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)          //數組越界
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);   //做擴容
}

看到這裏不經讓我想到一個題目:ArrayList(int initialCapacity)會不會初始化數組大小?也就是第二個構造函數

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}

初始化了一個空的數組,但是List的大小還是0,因爲List的大小是返回size的。

這道題是敖丙在這篇文章中提到的,我開始還有點懵,看完了擴容機制後基本瞭解了。
http://ftp.zouzhifeng.com/arraylist.png
初始化的時候,會初始化對應空間的空數組,但是不會該表size的大小。當用戶第一次添加的時候ensureCapacityInternal(size+1);
進入到calculateCapacity方法由於數組是不爲空的,返回長度minCapacity=1,進入到grow方法後,oldCapacity等於6,newCapacity等於1,newCapacity還是等於6,elementData做擴容等於6。但是size還是1.

注意:size不等於ArrayList的容量,size是ArrayList中存在元素的個數。
http://ftp.zouzhifeng.com/arraylist1.png
上面的代碼,雖然在默認情況下是10的容量,但是還是無法set進去,因爲set會檢查index的索引的正確性,是通過size來確認的,size不是ArrayList的大小,而是ArrayList中含有的元素的個數。

3.LinkedList

LinkedList集合,實現List集合,元素可以重複,順序按照插入的順序,單鏈表。他是線程不安全的。

四、List集合經典面試題

ArrayList的底層原理知道嗎?

知道,我看過ArrayList的源碼。主要回答可以從增加、刪除元素、擴容機制、構造方法上面答。

ArrayList裏面可以放int?

ArrayList放int是可以正常使用的,但是這裏面涉及了自動裝箱。ArrayList裏面存放的內容是對象,對於基本的數據類型,我們只能使用對應的包裝類。

自動裝箱和自動拆箱你解釋一遍吧?

自動裝箱,在程序編譯後轉化成了對應的包裝類。自動拆箱,在程序編譯後轉化城裏對應的還原類。
在這裏插入圖片描述
在這裏插入圖片描述
大於128的比較就是false,自動裝箱和拆箱的知識還是有一點了,小胖就先不寫了。這一點就當課後作業吧。

好的!我就是懶

Vector和ArrayList他們如何擴容?

Vector增長原來的1倍,ArrayList默認的初始化容量是10,ArrayList增長原來的0.5倍

Vector每次擴容一次,增長一倍

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

說說 ArrayList,Vector, LinkedList 的存儲性能和特性?

ArrayList和Vector都是使用的數組的方式儲存數字,他們可以直接通過序號索引元素,但是在插入的時候,需要元素的移動等內存的操作,所以索引數據快但是插入慢。Vector和ArrayList的本質區別就是使用的Synchronized的代碼塊形式,他是線程安全的,但是性能是比ArrayList差的。LinkedList使用雙向鏈表,可以按需要索引,向前或向後遍歷,插入數據的速度較快。總結:ArrayList在查詢的時候速度較快,LinkedList在插入或者刪除的時候速度較快。

不做重複的事情吧,以後我還會繼續更新上面的內容,你們可以先看看大佬寫的內容。
阿里面試,面試官沒想到一個ArrayList,我都能跟他扯半小時

五、參考資料

ArrayList源碼; 博客https://blog.csdn.net/qq_35190492/article/details/103883964

未完待續

本人知識有限,如果有錯誤,歡迎各位指正。謝謝~

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