Java集合框架——jdk 1.8 ArrayList 原始碼解析 System.arraycopy 怎麼使用的?
前言:作為菜鳥,需要經常回頭鞏固一下基礎知識,今天看看 jdk 1.8 的原始碼,這裡記錄 ArrayList 的實現。
一、簡介
ArrayList 是有序的集合;
底層採用陣列實現對資料的增刪查改;
不是執行緒安全的;
有自動擴容的功能。
二、類圖
三、詳細總結
1、ArrayList 是實現了 List 介面的可變資料,非同步實現,並允許包括 null 在內的所有元素。
2、底層採用陣列實現。
3、在陣列增加時,會進行擴容,但由於底層採用的陣列實現,所以擴容時會將老陣列中的元素拷貝到一份新的陣列中,所以效能代價很高
4、採用了 Fail-Fast 機制,面對併發的修改時,迭代器會丟擲異常,而不是冒著在將來某個不確定時間發生任意不確定行為的風險
5、remove 方法會通過 System.arraycopy() 方法讓下標到陣列末尾的元素向前移動一個單位,並把最後一位的值置空,方便 GC。
四、解惑
1、為什麼成員變數 elementData 為什麼被 transient 修飾?難道序列化時不需要陣列元素?
參考:https://blog.csdn.net/zero__007/article/details/52166306
transient 用來表示一個域不是該物件序行化的一部分,當一個物件被序行化的時候,transient 修飾的變數的值是不包括在序行化的表示中的。但是 ArrayList 又是可序行化的類,elementData 是 ArrayList 具體存放元素的成員,用 transient 來修飾 elementData,豈不是反序列化後的 ArrayList 丟失了原先的元素?
其實玄機在於 ArrayList 中的兩個方法:
/** * Save the state of the <tt>ArrayList</tt> instance to a stream (that * is, serialize it). * * @serialData The length of the array backing the <tt>ArrayList</tt> * instance is emitted (int), followed by all of its elements * (each an <tt>Object</tt>) in the proper order.*/ 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(); } } }
ArrayList 在序列化的時候會呼叫 writeObject,直接將 size 和 element 寫入 ObjectOutputStream;反序列化時呼叫 readObject,從 ObjectInputStream 獲取 size 和 element,再恢復到 elementData。
為什麼不直接用 elementData 來序列化,而採用上訴的方式來實現序列化呢?原因在於 elementData 是一個快取陣列,它通常會預留一些容量,等容量不足時再擴充容量,那麼有些空間可能就沒有實際儲存元素,採用上訴的方式來實現序列化時,就可以保證只序列化實際儲存的那些元素,而不是整個陣列,從而節省空間和時間。
2、為什麼有兩個預設空陣列的成員變數?為什麼 new ArrayList() 註釋說初始化容量為 10?
兩個雖然都為空陣列,但用途稍微有點不一致。
其中,EMPTY_ELEMENTDATA 用於構造器中給出了初始化容量為 0 時的陣列。程式碼如下:
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); } }
其中,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 用於預設構造器的陣列,主要用於在第一次增加元素時判斷是否需要給出預設容量 10 的大小(grow() 方法用於擴容)。這裡之所以不直接 new 一個初始容量為 10 的陣列,我想是因為有時我們會 new 一個 ArrayList(),但是並不會新增資料,這樣就可以節約空間。程式碼如下:
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { // 判斷是否是預設建構函式構造的預設空陣列例項,如果是就給出預設容量 10 和 size + 1 的最大值 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); }
3、如何擴容的?
原始碼中,每次在 add() 一個元素時,ArrayList 都需要對這個 List 的容量進行一個判斷。如果容量夠,直接新增,否則需要進行擴容,呼叫 grow() 方法。擴容呼叫的是 grow() 方法,通過 grow() 方法中呼叫的 Arrays.copyof() 方法進行對原陣列的複製,再通過呼叫 System.arraycopy() 方法進行復制,達到擴容的目的。
原始碼中,可以看出有三種情況:(這裡參考 https://blog.csdn.net/u010890358/article/details/80515284)
(一)如果當前陣列是由預設構造方法生成的空陣列並且第一次新增資料。此時 minCapacity 等於預設的容量(10),那麼根據原始碼中的邏輯可以看到最後陣列的容量會從 0 擴容成 10。而以後的擴容按照當前容量的1.5 倍進行擴容。1.5 倍這裡用了右移一位,不明白的可以自行百度。
(二)如果當前陣列是由自定義初始容量構造方法建立並且指定初始容量為 0。此時 minCapacity 等於 1,newCapacity = 0,那麼根據下面邏輯可以看到最後陣列的容量會從0變成1。這邊可以看到一個嚴重的問題,一旦我們執行了初始容量為 0,那麼根據下面的演算法前四次擴容每次都 +1,在第5次新增資料進行擴容的時候才是按照當前容量的1.5倍進行擴容。
(三)如果當擴容量(newCapacity)大於 ArrayList 陣列定義的最大值後會呼叫 hugeCapacity 來進行判斷。如果 minCapacity 已經大於 Integer 的最大值(溢位為負數)那麼丟擲 OutOfMemoryError(記憶體溢位)否則的話根據與 MAX_ARRAY_SIZE 的比較情況確定是返回 Integer 最大值還是 MAX_ARRAY_SIZE。這邊也可以看到 ArrayList 允許的最大容量就是 Integer 的最大值(-2 的 31 次方~ 2 的 31 次方減 1)。
原始碼如下:
//ArrayList 擴容的核心方法,此方法用來決定擴容量並擴容 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // 擴容的大小一般為當前容量的 1.5 倍 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) //當擴容量(newCapacity)大於ArrayList陣列定義的最大值後會呼叫hugeCapacity來進行判斷。如果minCapacity已經大於Integer的最大值(溢位為負數)那麼丟擲OutOfMemoryError(記憶體溢位)否則的話根據與MAX_ARRAY_SIZE的比較情況確定是返回Integer最大值還是MAX_ARRAY_SIZE。這邊也可以看到ArrayList允許的最大容量就是Integer的最大值(-2的31次方~2的31次方減1)。 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; } // ArrayList 的成員變數 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
4、Java 容器的快速報錯機制 fail-fast 是什麼?
請移步: Java集合框架——容器的快速報錯機制 fail-fast 是什麼?
5、System.arraycopy 怎麼使用的?
五、原始碼解析
1、主要成員變數
//預設的初始化容量 private static final int DEFAULT_CAPACITY = 10; //空陣列,用於 使用構造器給出初始容量為0時的預設空陣列 private static final Object[] EMPTY_ELEMENTDATA = {}; //預設的陣列,用於 使用預設構造器建立的預設空陣列,主要用於後面第一次增加資料時判斷是否需要給出預設容量 10 的大小 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //用於儲存 ArrayList 的元素,這裡就可以看出 ArrayList 的底層就是陣列 transient Object[] elementData; // non-private to simplify nested class access //大小 private int size; //記錄被修改的次數,用於迭代器迭代時保證資料沒有被修改過 protected transient int modCount = 0; //陣列大小的最大值 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
2、構造方法(3個)
預設構造方法:
註釋說例項化了一個容量為 10 的陣列,但其實這裡返回的是一個空陣列,是在陣列第一次增加資料時通過擴容達到的初始容量為 10 的陣列。前面解惑的2、為什麼有兩個預設空陣列的成員變數?也提到了。
/** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
自定義初始容量的構造方法:
/** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */ 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); } }
生成一個帶資料的 ArrayList 例項:
/** * Constructs a list containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. * * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */ 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; } }
參考:
https://blog.csdn.net/u010890358/article/details/80515284
所有的集合框架:
http://www.runoob.com/java/java-collections.html
https://blog.csdn.net/qq_25868207/article/details/55259978