1. 程式人生 > 其它 >JDK原始碼剖析-集合篇-ArrayList

JDK原始碼剖析-集合篇-ArrayList

1.ArrayList基本原理以及優缺點

1.1ArrayList基本原理

一句話講,在JDK中,ArrayList底層基於一個Object[]陣列來維護資料。

1.2ArrayList優缺點

缺點:

  1. 容量受限時,需要進行陣列擴容,進行元素拷貝會影響效能

  2. 頻繁刪除和往中間插入元素時,產生元素挪動,也會進行元素拷貝,影響效能

  3. 不是執行緒安全的

優點:

  1. 隨機訪問某個元素,效能很好

建議:

維護順序插入的資料,遍歷或者索引訪問很方便,預估資料大小,避免頻繁擴容。不適合頻繁的刪除和中間插入等操作。

2.ArrayList原始碼解析

2.1如何看原始碼

看核心原始碼和精讀原始碼的方式不太一樣。看核心原始碼,也就是核心部分的原始碼,一般也就是10-30%。你應該都是從一些入口開始看起,但是如果你要精讀原始碼,除了在閱讀核心原始碼的基礎上,你還需要將原始碼拆解開來,研究每一個元件的各個作用,之後再從入口開始,一行一行都讀懂。應該先看核心原始碼再精讀原始碼

,從整體到細節,再主幹到分支

看原始碼無論是主動還是被動,都是先有問題,再有技術。為了解決什麼問題,從而衍生出瞭解決問題的技術。

閱讀原始碼的技巧,說白了就是一些思想,常見的有:

  • 自頂而下

  • 從整體到細節

  • 在主幹到分支

  • 連猜帶蒙

  • 結合功能場景逐個分析

方法就更多了,主要有:

靜態

  • 看註釋

  • 加註釋

  • 畫圖

動態

  • Debug

  • 觀察日誌或增加日誌

閱讀原始碼,需要很多技術基礎,的確沒錯。但是也分是什麼樣的原始碼,如果只是簡單的原始碼,需要的基礎知識會很少,比如JDK的原始碼。但是如果比較難的原始碼,Zookeeper、HDFS、Kafka、Dubbo這些原始碼,就需要掌握Netty,NIO,網路,Java集合和併發等基礎才能看得更好。當然如果沒有這些基礎也不是不能看的,就是理解可能不會深,對原始碼最後可能只是初步瞭解。所以還是那句話,不要一概而論。

2.2ArrayList原始碼整體

當你看ArrayList的原始碼,可以先看看它有哪些核心成員變數,剛開始,你肯定不知道哪些是核心的變數,所以簡單過一下即可,根據名字,型別,大概看看即可,看不懂也沒關係。之後看下有哪些方法,根據名字大概猜猜是做什麼的。做這些主要讓你瞭解基本的脈絡,對原始碼有個大體映象,千萬不要在這裡細究原始碼。你主要有哪些方法和成員變數、內部類就可以了。

比如,你可以看到ArrayList原始碼中,有幾個靜態成員,從名字上看像是容量大小相關的,還有size和elementData等成員變數,有幾個建構函式,

有一些ensureCapacity,calculateCapacity等私有方法,感覺也是和容量大小有關的。因為Capacity就是容量,大小的意思。還有很多你應該常用的方法add(),set(),get()等等。

2.3從ArrayList建構函式開始

之前我們提到過,看核心原始碼一般是從入口開始,也就常說的自頂而下。

首先你要有個程式碼Demo。之後從它的入口開始看起,程式碼清單如下:

import java.util.List;


public class ArrayListDemo {
  public static void main(String[] args) {
      List<String> list = new ArrayList<>();
  }
}

這個很簡單的程式碼,入口是main函式,第一行就是建立了一個ArrayList,裡面的元素都是String型別,使用預設無參的建構函式,你點選建構函式,進入原始碼來看看:

private static final 
           Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

可以看到,有一個成員變數叫elementData的Object[]陣列,這個印證了我們之前提到過的ArrayList底層基本原理是陣列。而且你記不記得之前,在看ArrayList原始碼脈絡的時候是不是已經看到過了這個變數呢?

預設無參的建構函式讓elementData指向了和空陣列DEFAULTCAPACITY_EMPTY_ELEMENTDATA一樣的地址。

如果各位如果有印象的話,DEFAULTCAPACITY_EMPTY_ELEMENTDATA這個變數也是你之前看到過的靜態成員變數。

上面還列了一個成員變數size,你可以連蒙帶猜一下,它應該是描述陣列大小的變數。而且size是int型別,大家都知道int的預設值是0。

所以小結一下,你知道了new ArrayList<>()這個動作,底層是使用了Object[]陣列,但是這個陣列預設是空的,大小為0。

上面我們主要通過看脈絡,連蒙帶猜的思想,看了一個簡單的不能再簡單的原始碼。不知道你有沒有感覺到這兩個思想。有了初步的感受。接下來我還要引入一個非常關鍵的方法:畫圖

上面ArrayList的無參建構函式的原始碼,基本上如下圖所示:

2.4ArrayList第一次呼叫add方法

首先你要修改下你的Demo,修改如下:

import  java.util.ArrayList;

import java.util.List;

public class  ArrayListDemo {

 public static void main(String[] args) {

         List<String> hostList =  new ArrayList<>(); //預設大小是0

         hostList.add("host1");

  }

}

上面程式碼,假設你通過add方法向hostList添加了1個host主機地址。ArrayList第一次呼叫add方法發生了什麼?

你可以點選進去,看到如下程式碼清單:

   /**

     * Appends the specified element to the  end of this list.

     *

     * @param e element to be appended to  this list

     * @return <tt>true</tt> (as  specified by {@link Collection#add})

     */

    public boolean add(E e) {

        ensureCapacityInternal(size +  1);  // Increments modCount!!

        elementData[size++] = e;

        return true;

    }

看之前,我又要多說兩句,原始碼的註釋和方法名,有時候能幫助你瞭解這個方法大致的作用。這是很關鍵的一點。比如註釋的意思是add方法是追加一個具體的元素到list的末尾;方法名ensureCapacityInternal,是確保內部容量的意思。看過之後,你應該可以猜到這方法大致是確認容量相關的,可能是判斷容量能否新增元素用的。
還要注意的是,你看一個方法的時候,不要著急直接從第一個行就開始看,也是先根據註釋和方法名有一個大概的認識後,你需要看下整個方法的結構之後再開始。比如呼叫了哪些其他方法,有幾個for迴圈或if結構。就像這個add方法,他主要有2步,第一步是呼叫了一個方法,應該是確保內部容量是否夠新增元素,第二步是一個數組基本賦值操作並且size++一下。如下圖所示:

2.5ArrayList的陣列大小為0也能可以新增元素?

接著當你知道了整個方法的脈絡,你再來看下細節,先看第一步:ensureCapacityInternal()。

這個方法入參是size + 1,size之前說你應該看到過,就是一個成員變數int size。它的預設值是0,所以size+1後,這個方法的實參就是1,那麼形參minCapacity的值就是1。(實參就是傳入方法實際的值,形參就是方法括號中使用的引數名)。可以看到ensureCapacityInternal的程式碼清單如下:

private void  ensureCapacityInternal(int minCapacity) {

  ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));

}

接著你可以看到這裡又呼叫了calculateCapacity方法和ensureExplicitCapacity方法。我們先進入calculateCapacity看下,你可以記下它的實參是minCapacity=1,elementData還記得麼?是ArrayList核心的那個成員變數Object[]陣列。calculateCapacity程式碼如下:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA  = {};

transient Object[] elementData; 

private static final int DEFAULT_CAPACITY = 10;



private static int  calculateCapacity(Object[] elementData, int minCapacity) {

        if (elementData ==  DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

            return Math.max(DEFAULT_CAPACITY, minCapacity);

        }

        return minCapacity;

 }

這裡你可以看熟悉的兩個成員變數:elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
在ArrayList無參建構函式的時候就看到過。第一次呼叫add方法的時候,它們倆的引用地址和值都是應該一樣的,因為在建構函式中有過這麼一句話this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
所以這裡進入第一個if條件,然後使用DEFAULT_CAPACITY和minCapacity作比較,可以看到DEFAULT_CAPACITY這個變數的值是10,也就是說這裡Math.max操作後會返回10。
到這裡你可以在完善下之前畫的圖,就會變成如下所示:

也就是說calculateCapacity返回後,回到ensureCapacityInternal這個方法,會傳入ensureExplicitCapacity的實參是10,因為calculateCapacity(elementData, minCapacity)= 10。

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

接著你又會進入ensureExplicitCapacity方法,看到如下程式碼:

private void ensureExplicitCapacity(int  minCapacity) {

        modCount++;

        // overflow-conscious code

        if (minCapacity - elementData.length  > 0)

            grow(minCapacity);

    }

此時形參minCapacity也就是剛才傳入的10,你可以從名字上看這個方法,ensureExplicitCapacity意思是確保精確的容量的意思。還是先看下方法的脈絡,第一行有一個modCount++,之後你可以看到一行程式碼註釋,overflow-consciouscode意思是說具有溢位意思的程式碼,之後有一個if條件,滿足會進入grow方法。
到這裡你可以連蒙帶猜下,這裡的grow方法,是不是就是說容量不夠,會進行增長,也就是擴容呢?
因為我們知道,無參建構函式建立的ArrayList大小預設是一個為0的Object[]陣列elementdata。而且elementdata.length肯定是0,難道也能新增元素麼?肯定是不可以的。
接著我們逐行看下,第一行的modCount好像不太知道是什麼,你可以點選下它,看到它是一個成員變數,而且有一大堆註釋,好像也沒看懂什麼意思。所以這裡要告訴大家另一個看原始碼的思想了:抓大放小。比如這裡看上去modeCount和新增操作沒啥關係,就先跳過!(其實這個modCount是併發操作ArrayList時,fail-fast機制的實現)

目前執行程式碼如下圖所示:

2.6ArrayList 第一次新增元素如何擴容

你接著往下看,很明顯,minCapacity=10,elementData這個空陣列length肯定是0。所以10-0>0,會滿足if條件,進入grow方法,它的實參是10。它的程式碼清單如下:

   private void grow(int minCapacity) {

        // overflow-conscious code

        int oldCapacity  = elementData.length;

        int newCapacity  = oldCapacity + (oldCapacity >> 1);

        if (newCapacity  - minCapacity < 0)

            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);

    }

grow方法形參minCapacity的值是10,你進入這個grow方法後,從脈絡上來看,有兩個if分支,有兩個區域性變數oldCapacity和newCapacity,還有一步elementData通過Arrays.copyOf方法的賦值操作。

而且oldCapacity應該是0,因為我們知道elementData陣列是空的。接著你會看到一句:intnewCapacity =oldCapacity+(oldCapacity >> 1);

這句話是什麼意思呢?其實就是oldCapacity右位移1位。如果你還記得計算機基礎中的話,右移一位,有點類似於除以2,底層是二進位制的運算而已。這裡可以舉個例子:

比如oldCapacity=10,如果先左移1,就是乘以2,在右移 1就是除以2,如下所示:

左移和右移1位舉例:

二進位制 1010 十進位制:10 原始數 oldCapacity

二進位制 10100 十進位制:20 左移一位 oldCapacity = oldCapacity << 1;

二進位制 1010 十進位制:10 右移一位 oldCapacity = oldCapacity >> 1;

那麼int newCapacity = oldCapacity +(oldCapacity >> 1); 其實這句話的意思就是在原有大小基礎上增加一半大小。

但是由於oldCapacity=0,所以增加一半大小,newCapacity=0+0>>1=0+0=0

而此時minCapacity=10。這樣就會進入第一個if條件,因為newCapacity - minCapacity < 0,即0-10<0,然後進行了一步賦值操作,newCapacity就會變成和minCapacity一樣,值是10。

這裡你可以總結一下,也就是說如果建立ArrayList時,不指定大小, ArrayList第一次新增元素,會指定預設的容量大小為10。

你可以繼續完善你的圖,grow方法邏輯如下:

2.7ArrayList計算完擴容容量大小後,又幹了什麼?

除了上面計算擴容容量大小的程式碼,是grow的核心邏輯之一。

第一次新增元素時,在grow中,最後還會執行一行程式碼:

elementData=Arrays.copyOf(elementData,newCapacity);

這個也是grow方法中另一個核心步驟,陣列的拷貝。

你可以點選Arrays.copyOf,看看它底層做了些什麼。程式碼如下:

public static <T>  T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength,  original.getClass());
}

 public static <T,U> T[] copyOf(U[]  original, int newLength  , 
                                                         Class<? extends T[]> newType) {
        T[] copy =  ((Object)newType == (Object)Object[].class)
            ? (T[])  new Object[newLength]
            : (T[])  Array.newInstance(newType.getComponentType(), newLength);

        System.arraycopy(original,  0, copy, 0,Math.min(original.length, newLength));

        return copy;
    }

還是從脈絡上看下,這裡Arrays.copyOf它實參是elementData(Object[]空陣列)和newCapacity=10。可以看到它內部直接呼叫了一個過載方法。

在過載方法中,首先呼叫了一個三元表示式和一個System.arraycopy方法呼叫,接著執行了

System.arraycopy(original, 0, copy, 0,Math.min(original.length,newLength));

這句話,它看樣子像是操作了original和 copy的樣子。

之後你再來仔細分析下細節。

2.8ArrayList的缺點,原因原來是在這裡!

根據傳遞的引數elementData的class是Object[].class的型別,可以看出三元表示式會執行(T[]) new Object[newLength]。

接著就會執行System.arraycopy這個方法。你可以查閱下JDK的API,它的主要是作用是陣列的拷貝。

由於這個方法的API不是很好理解。這裡我給大家講下,它的方法簽名如下:

System.arraycopy(Objectsrc,int srcPos,Object dest,int destPos, int length)

這裡,如果大家遇見難以理解的程式碼,除了畫圖,另外一個方法就是舉例子。比如:

舉個例子:

假設:有兩個陣列src和dest,它們都有5個元素,0,1,2,3,4。即:src[0,1,2,3,4] dest[0,1,2,3,4]。

問題:執行System.arraycopy(src, 3, dest,  1,2)後是什麼意思呢?

答案:意思就是從src源陣列3位置開始,移動2個元素到dest陣列位置1, 從dest陣列的1位置開始覆蓋。

結果dest就會變為0,3,4,3,4

好了,你知道了這個API基本的使用,再來看原始碼中的程式碼:

System.arraycopy(original,0,copy,0,Math.min(original.length,newLength));

首先original就是我們傳遞進來的elementData。它的length是0。newLength是傳遞進來的newCapacity,也就是10。copy 是我們剛建立的Object[]陣列,長度為10。Math.min取最小後是0。也就是變成:

System.arraycopy(original,0,copy, 0,0);

這個就很好理解了,就是從original位置0開始移動0個元素到copy陣列,從copy陣列0位置開始覆蓋。這就等於什麼都沒拷貝,直接返回了我們新建立的陣列T[] copy,這個陣列大小為10。

最終Arrays.copyOf獲得了一個長度為10的陣列,但裡面沒有任何元素。接著這句話就執行完了。elementData = Arrays.copyOf(elementData, newCapacity);結束grow方法的也就執行完了。

而grow方法執行完就意味著ensureCapacityInternal執行完了。這個方法已經確保了內部陣列的容量大小可以放入新元素。

我們回到開始,執行完第一步,內部容量確保後,接著第二步直接執行了elementData[size++]= e;通過陣列的賦值操作,就完成第一次元素的添加了!

到這裡可以發現,當新增的時候,如果大小不夠就會進入grow方法,進入grow方法就會進行一次1.5倍的擴容。而且程式碼規範一般都建議我們要指定ArrayList的大小,如果不指定,可能會造成頻繁的擴容和拷貝,造成效能低下。

   public boolean add(E e) {

        ensureCapacityInternal(size +  1);  // Increments modCount!!

        elementData[size++]  = e;

        return true;

    }

好了,到這裡ArrayList最常用的add(E e)方法的原始碼原理就已經研究透徹了。

最終的原始碼原理圖如下: