1. 程式人生 > >Java -- 基於JDK1.8的ArrayList原始碼分析

Java -- 基於JDK1.8的ArrayList原始碼分析

1,前言

  很久沒有寫部落格了,很想念大家,18年都快過完了,才開始寫第一篇,爭取後面每週寫點,權當是記錄,因為最近在看JDK的Collection,而且ArrayList原始碼這一塊也經常被面試官問道,所以今天也就和大家一起來總結一下

2,原始碼解讀

  當我們一般提到ArrayList的話都會脫口而出它的幾個特點:有序、可重複、查詢速度快,但是插入和刪除比較慢,執行緒不安全,那麼現在阿呆哥哥就會有這些疑問:為什麼說是有序的?怎麼有序?為什麼又說插入和刪除比較慢?為什麼慢?還有執行緒為什麼不安全?所以帶著這些問題,我們一一的來原始碼中來找找答案。

  一般對於一個陌生的類,我們想使用它,都會先看它構造方法,再看它的屬性和方法,那麼我們也按照這種方式來讀讀ArrayList這個類

  2.1構造方法

1

2

ArrayList<String> arrayList = new ArrayList();

ArrayList<String> arrayList1 = new ArrayList(2);

  一般來說我們常見使用ArrayList的建立方式是上面的這兩種

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

private static

 final int DEFAULT_CAPACITY = 10;

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

private static final Object[] EMPTY_ELEMENTDATA = {};

transient Object[] elementData;

private int size;

 

public ArrayList() {

        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

}

 

 

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

        }

}

  上面是我們兩個構造方法和我們類中基本的屬性,從上面的程式碼上來看,在建立構造基本上都沒有做,且定義了兩個預設的空陣列,預設容器的大小DEFAULT_CAPACITY為10,還有我們真正儲存元素的地方elementData陣列,所以這就是為什麼說ArrayList儲存集合元素的底層時是使用陣列來實現,OK,上面的程式碼除了一個transient 修飾符之外我們同學們可能有點陌生之外,其餘的應該都能看的懂,transient 有什麼作用還有為什麼用它修飾elementData欄位,這個需要看完整個原始碼之後,我再來給大家解釋的話比較合適,這裡只需要留心一下。

  還有一個不常用的構造方法

1

2

3

4

5

6

7

8

9

10

11

public ArrayList(Collection<? extends E> 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;

        }

}

  第2行:利用Collection.toArray()方法得到一個物件陣列,並賦值給elementData 

  第3行:size代表集合的大小,當通過別的集合來構造ArrayList的時候,需要賦值size

  第5-6行:判斷 c.toArray()是否出錯返回的結果是否出錯,如果出錯了就利用Arrays.copyOf 來複制集合c中的元素到elementData陣列中

  第9行:如果c中元素數量為空,則將EMPTY_ELEMENTDATA空陣列賦值給elementData

  上面就是所有的建構函式的程式碼了,這裡我們可以看到,當建構函式走完之後,會創建出陣列elementData和初始化size,Collection.toArray()則是將Collection中所有元素賦值到一個數組,Arrays.copyOf()則是根據Class型別來決定是new還是反射來創造物件並放置到新的陣列中,原始碼如下:

1

2

3

4

5

6

7

8

9

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {

        @SuppressWarnings("unchecked")

        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;

}

  這裡面System.arraycopy(Object src,  int  srcPos, Object dest, int destPos,  int length) 這個方法在我們的後面會的程式碼中會出現,就先講了,定義是:將陣列src從下標為srcPos開始拷貝,一直拷貝length個元素到dest陣列中,在dest陣列中從destPos開始加入先的srcPos陣列元素。相當於將src集合中的[srcPos,srcPos+length]這些元素新增到集合dest中去,起始位置為destPos

  2.2 增加元素方法

  一般經常使用的是下面三種方法

1

2

3

arrayList.add( E element);

arrayList.add(int index, E element);

arrayList.addAll(Collection<? extends E> c);

  讓我們一個個來看看

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

public boolean add(E e) {

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

        elementData[size++] = e;

        return true;

}

 

private void ensureCapacityInternal(int minCapacity) {

        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

        }

 

        ensureExplicitCapacity(minCapacity);

}

 

private void ensureExplicitCapacity(int minCapacity) {

        modCount++;

 

        // overflow-conscious code

        if (minCapacity - elementData.length > 0)

            grow(minCapacity);

}

 

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

}

 

private static int hugeCapacity(int minCapacity) {

        if (minCapacity < 0// overflow

            throw new OutOfMemoryError();

        return (minCapacity > MAX_ARRAY_SIZE) ?

            Integer.MAX_VALUE :

            MAX_ARRAY_SIZE;

}

 

Integer. MAX_VALUE = 0x7fffffff;

MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8

  第2行:呼叫ensureCapacityInternal()函式

  第8-9行:判斷當前是否是使用預設的建構函式初始化,如果是設定最小的容量為預設容量10,即預設的elementData的大小為10(這裡是有一個容器的概念,當前容器的大小一般是大於當前ArrayList的元素個數大小的)

  第16行:modCount欄位是用來記錄修改過擴容的次數,呼叫ensureExplicitCapacity()方法意味著確定修改容器的大小,即確認擴容

  第26-30、35-44行:一般預設是擴容1.5倍,當時當發現還是不能滿足的話,則使用size+1之後的元素個數,如果發現擴容之後的值大於我們規定的最大值,則判斷size+1的值是否大於MAX_ARRAY_SIZE的值,大於則取值MAX_VALUE,反之則MAX_ARRAY_SIZE,也就數說容器最大的數量為MAX_VALUE

  第32行:就是拷貝之前的資料,擴大陣列,且構建出一個新的陣列

  第3行:這時候陣列擴容完畢,就是要將需要新增的元素加入到陣列中了

1

2

3

4

5

6

7

8

9

10

public void add(int index, E element) {

        if (index > size || index < 0)

            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

 

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

        System.arraycopy(elementData, index, elementData, index + 1,

                         size - index);

        elementData[index] = element;

        size++;

}

  第2-3行:判斷插入的下標是否越界

  第5行:和上面的一樣,判斷是否擴容

  第6行:System.arraycopy這個方法的api在上面已經講過了,這裡的話則是將陣列elementData從index開始的資料向後移動一位

  第8-9行:則是賦值index位置的資料,陣列大小加一

1

2

3

4

5

6

7

8

public boolean addAll(Collection<? extends E> c) {

        Object[] a = c.toArray();

        int numNew = a.length;

        ensureCapacityInternal(size + numNew);  // Increments modCount

        System.arraycopy(a, 0, elementData, size, numNew);

        size += numNew;

        return numNew != 0;

}

  第2行:將集合轉成陣列,這時候原始碼沒有對c空很奇怪,如果傳入的Collection為空就直接空指標了

  第3-7行:獲取陣列a的長度,進行擴容判斷,再將新傳入的陣列複製到elementData陣列中去

  所以對增加資料的話主要呼叫add、addAll方法,判斷是否下標越界,是否需要擴容,擴容的原理是每次擴容1.5倍,如果不夠的話就是用size+1為容器值,容器擴充後modCount的值對應修改一次

  2.3 刪除元素方法  

  常用刪除方法有以下三種,我們一個個來看看

1

2

3

arrayList.remove(Object o);

arrayList.remove(int index)

arrayList.removeAll(Collection<?> c)

  

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

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;

}

 

private void fastRemove(int index) {

        modCount++;

        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和不為空,都是通過for迴圈找到要被移除元素的第一個下標,所以這裡我們就會思考,當我們的集合中有多個null的話,是不是呼叫remove(null)這個方法只會移除第一個出現的null元素呢?這個需要同學們下去驗證一下。然後通過System.arraycopy函式,來重新組合elementData中的值,且elementData[size]置空原尾部資料 不再強引用, 可以GC掉。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public E remove(int index) {

        if (index >= size)

            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

 

        modCount++;

        E oldValue = (E) 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

 

        return oldValue;

}

  可以看到remove(int index)更簡單了,都不需要通過for迴圈將要刪除的元素下邊確認下來,整體的邏輯和上面通過元素刪除的沒什麼區別,再來看看批量刪除

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

public boolean removeAll(Collection<?> c) {

        Objects.requireNonNull(c);

        return batchRemove(c, false);

}

 

public static <T> T requireNonNull(T obj) {

        if (obj == null)

            throw new NullPointerException();

        return obj;

}

 

private boolean batchRemove(Collection<?> c, boolean complement) {

        final Object[] elementData = this.elementData;

        int r = 0, w = 0;

        boolean modified = false;

        try {

            for (; r < size; r++)

                if (c.contains(elementData[r]) == complement)

                    elementData[w++] = elementData[r];

        finally {

            // Preserve behavioral compatibility with AbstractCollection,

            // even if c.contains() throws.

            if (r != size) {

                System.arraycopy(elementData, r,

                                 elementData, w,

                                 size - r);

                w += size - r;

            }

            if (w != size) {

                // clear to let GC do its work

                for (int i = w; i < size; i++)

                    elementData[i] = null;

                modCount += size - w;

                size = w;

                modified = true;

            }

        }

        return modified;

    }

  第2、6-10行:對傳入集合c進行判空處理

  第13-15行:定義區域性變數elementData、r、w、modified   elementData用來重新指向成員變數elementData,用來儲存最終過濾後的元素,w用來紀錄過濾之後集合中元素的個數,modified用來返回這次是否有修改集合中的元素

  第17-19行:for迴圈遍歷原有的elementData陣列,發現如果不是要移除的元素,則重新儲存在elementData,且w自增

  第23-28行:如果出現異常,則會導致 r !=size , 則將出現異常處後面的資料全部複製覆蓋到數組裡。

  第29-36行:判斷如果w!=size,則表明原先elementData陣列中有元素被移除了,然後將陣列尾端size-w個元素置空,等待gc回收。再修改modCount的值,在修改當前陣列大小size的值

  2.3 修改元素方法

1

arrayList.set(int index, E element)

  常見的方法也就是上面這一種,我們來看看它的實現的原始碼

1

2

3

4

5

6

7

8

public E set(int index, E element) {

        if (index >= size)

            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

 

        E oldValue = (E) elementData[index];

        elementData[index] = element;

        return oldValue;

}

  原始碼很簡單,首先去判斷是否越界,如果沒有越界則將index下表的元素重新賦值element新值,將老值oldValue返回回去

  2.4 查詢元素方法

1

arrayList.get(int index);

  讓我們看看原始碼

1

2

3

4

5

6

public E get(int index) {

        if (index >= size)

            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

 

        return (E) elementData[index];

}

  原始碼也炒雞簡單,首先去判斷是否越界,如果沒有越界則將index下的元素從elementData陣列中取出返回

  2.5 清空元素方法

1

arrayList.clear();

  常見清空也就這一個方法

1

2

3

4

5

6

7

8

9

public void clear() {

        modCount++;

 

        // clear to let GC do its work

        for (int i = 0; i < size; i++)

            elementData[i] = null;

 

        size = 0;

}

  原始碼也很簡單,for迴圈重置每一個elementData陣列為空,修改size的值,修改modCount值

  2.6 判斷是否存在某個元素

1

2

arrayList.contains(Object o);

arrayList.lastIndexOf(Object o);

  常見的一般是contains方法,不過我這裡像把lastIndexOf方法一起講了,原始碼都差不多

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

public boolean contains(Object o) {

        return indexOf(o) >= 0;

}

 

public int indexOf(Object o) {

        if (o == null) {

            for (int i = 0; i < size; i++)

                if (elementData[i]==null)

                    return i;

        else {

            for (int i = 0; i < size; i++)

                if (o.equals(elementData[i]))

                    return i;

        }

        return -1;

}

 

public int lastIndexOf(Object o) {

        if (o == null) {

            for (int i = size-1; i >= 0; i--)

                if (elementData[i]==null)

                    return i;

        else {

            for (int i = size-1; i >= 0; i--)

                if (o.equals(elementData[i]))

                    return i;

        }

        return -1;

    }

  通過上面的原始碼,大家可以看到,不管是contains方法還是lastIndexOf方法,其實就是進行for迴圈,如果找到該元素則記錄下當前元素下標,如果沒找到則返回-1,很簡單

  2.7 遍歷ArrayList中的物件(迭代器)

1

2

3

4

Iterator<String> it = arrayList.iterator();

        while (it.hasNext()) {

            System.out.println(it.next());

}

  我們遍歷集合中的元素方法挺多的,這裡我們就不講for迴圈遍歷,我們來看看專屬於集合的iterator遍歷方法吧

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

public Iterator<E> iterator() {

        return new Itr();

}

 

private class Itr implements Iterator<E> {

        // Android-changed: Add "limit" field to detect end of iteration.

        // The "limit" of this iterator. This is the size of the list at the time the

        // iterator was created. Adding & removing elements will invalidate the iteration

        // anyway (and cause next() to throw) so saving this value will guarantee that the

        // value of hasNext() remains stable and won't flap between true and false when elements

        // are added and removed from the list.

        protected int limit = ArrayList.this.size;

 

        int cursor;       // index of next element to return

        int lastRet = -1// index of last element returned; -1 if no such

        int expectedModCount = modCount;

 

        public boolean hasNext() {

            return cursor < limit;

        }

 

        @SuppressWarnings("unchecked")

        public E next() {

            if (modCount != expectedModCount)

                throw new ConcurrentModificationException();

            int i = cursor;

            if (i >= limit)

                throw new NoSuchElementException();

            Object[] elementData = ArrayList.this.elementData;

            if (i >= elementData.length)

                throw new ConcurrentModificationException();

            cursor = i + 1;

            return (E) elementData[lastRet = i];

        }

 

        public void remove() {

            if (lastRet < 0)

                throw new IllegalStateException();

            if (modCount != expectedModCount)

                throw new ConcurrentModificationException();

 

            try {

                ArrayList.this.remove(lastRet);

                cursor = lastRet;

                lastRet = -1;

                expectedModCount = modCount;

                limit--;

            catch (IndexOutOfBoundsException ex) {

                throw new ConcurrentModificationException();

            }

        }

  第1-3行:在獲取集合的迭代器的時候,去new了一個Itr物件,而Itr實現了Iterator介面,我們主要重點關注Iterator介面的hasNext、next方法

  第12-16行:定義變數,limit:用來記錄當前集合的大小值;cursor:遊標,預設為0,用來記錄下一個元素的下標;lastRet:上一次返回元素的下標

  第18-20行:判斷當前遊標cursor的值是否超過當前集合大小zise,如果沒有則說明後面還有元素

  第24-31行:在這裡面做了不少執行緒安全的判斷,在這裡如果我們非同步的操作了集合就會觸發這些異常,然後獲取到集合中儲存元素的elemenData陣列

  第32-33行:遊標cursor+1,然後返回元素 ,並設定這次次返回的元素的下標賦值給lastRet

 

3,看原始碼之前問題的反思

  ok,上面的話基本上把我們ArrayList常用的方法的原始碼給看完了。這時候,我們需要來對之前的問題來一一進行總結了

  ①有序、可重複是什麼概念?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

public static void main(String[] args){

        ArrayList arrayList = new ArrayList();

        arrayList.add("1");

        arrayList.add("1");

        arrayList.add("2");

        arrayList.add("3");

        arrayList.add("1");

        Iterator<String> it = arrayList.iterator();

        while (it.hasNext()) {

            System.out.println(it.next());

        }

}

 

輸出結果

1

1

2

3

1

  可重複是指加入的元素可以重複,有序是指的加入元素的順序和取出來的時候順序相同,一般這個特點是List相對於Set和Map來比較出來的,後面我們把Set、Map的原始碼看了之後會更加理解這兩個特點

  ② 為什麼說查詢查詢元素比較快,但新增和刪除元素比較慢呢?

  我們從上面的原始碼得到,當增加元素的時候是有可能會觸發擴容機制的,而擴容機制會導致陣列複製;刪除和批量刪除會導致找出兩個集合的交集,以及陣列複製操作;而查詢直接呼叫return (E) elementData[index]; 所以說增、刪都相對低效 而查詢是很高效的操作。

  ③ 為什麼說ArrayList執行緒是不安全

  從上面的程式碼我們都知道,現在add()方法為例

1

2

3

4

5

6

public boolean add(E e) {

        //確定是否擴容,這裡可以忽略

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

        elementData[size++] = e;

        return true;

    }   

  這裡我們主要看兩點,第一點add()方法前面沒有synchronized欄位、第二點 elementData[size++] = e;這段程式碼可以拆開為下面兩部分程式碼

1

2

elementData[size] = e;

size++

  也就是說整個add()方法可以拆為兩步,第一步在elementData[Size] 的位置存放此元素,第二步增大 Size 的值。我們都知道我們的CUP是切換程序執行的,在單執行緒中這樣是沒有問題的,但是一般在我們專案中很多情況是在多執行緒中使用ArrayList的,這時候比如有兩個執行緒,執行緒 A 先將元素存放在位置 0。但是此時 CPU 排程執行緒A暫停,執行緒 B 得到執行的機會。執行緒B也向此 ArrayList 新增元素,因為此時 Size 仍然等於 0 ,所以執行緒B也將元素存放在位置0。然後執行緒A和執行緒B都繼續執行,都增加 Size 的值。這樣就會得到元素實際上只有一個,存放在位置 0,而 Size 卻等於 2。這樣就造成了我們的執行緒不安全了。

  大家可以寫一個執行緒搞兩個執行緒來試試,看看size是不是有問題,這裡就不帶大家一起寫了。

  ④ transient 關鍵字有什麼用?

  唉,這個就有點意思了,這個是我們之前讀原始碼讀出來的遺留問題,那原始碼現在讀完了,是時候來解決這個問題了,我們來看看transient官方給的解釋是什麼

1

當物件被序列化時(寫入位元組序列到目標檔案)時,transient阻止例項中那些用此關鍵字宣告的變數持久化;當物件被反序列化時(從原始檔讀取位元組序列進行重構),這樣的例項變數值不會被持久化和恢復。

  然後我們看一下ArrayList的原始碼中是實現了java.io.Serializable序列化了的,也就是transient Object[] elementData; 這行程式碼的意思是不希望elementData被序列化,那這時候我們就有一個疑問了,為什麼elementData不進行序列化?這時候我去網上找了一下答案,覺得這個解釋是最合理且易懂的

1

在ArrayList中的elementData這個陣列的長度是變長的,java在擴容的時候,有一個擴容因子,也就是說這個陣列的長度是大於等於ArrayList的長度的,我們不希望在序列化的時候將其中的空元素也序列化到磁碟中去,所以需要手動的序列化陣列物件,所以使用了transient來禁止自動序列化這個陣列

  這時候我們是懂了為什麼不給elementData進行序列化了,那當我們要使用序列化物件的時候,elementData裡面的資料是不是不能使用了?這裡ArrayList的原始碼提供了下面方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

private void writeObject(java.io.ObjectOutputStream s)

        throws java.io.IOException{

        // Write out element count, and any hidden stuff

        int expectedModCount = modCount;

        s.defaultWriteObject();

 

        // Write out size as capacity for behavioural compatibility with clone()

        s.writeInt(size);

 

        // Write out all elements in the proper order.

        for (int i=0; i<size; i++) {

            s.writeObject(elementData[i]);

        }

 

        if (modCount != expectedModCount) {

            throw new ConcurrentModificationException();

        }

    }

 

    /**

     * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,

     * deserialize it).

     */

    private void readObject(java.io.ObjectInputStream s)

        throws java.io.IOException, ClassNotFoundException {

        elementData = EMPTY_ELEMENTDATA;

 

        // Read in size, and any hidden stuff

        s.defaultReadObject();

 

        // Read in capacity

        s.readInt(); // ignored

 

        if (size > 0) {

            // be like clone(), allocate array based upon size not capacity

            ensureCapacityInternal(size);

 

            Object[] a = elementData;

            // Read in all elements in the proper order.

            for (int i=0; i<size; i++) {

                a[i] = s.readObject();

            }

        }

    }

    通過writeObject方法將資料非null資料寫入到物件流中,再使用readObject讀取資料

 

4,總結

  上面我們寫了這麼一大篇,是時候該來總結總結一下了

  ①查詢高效、但增刪低效,增加元素如果導致擴容,則會修改modCount,刪出元素一定會修改。 改和查一定不會修改modCount。增加和刪除操作會導致元素複製,因此,