ArrayList是最多見的集合類,顧名思義,ArrayList就是一個以數組形式實現的集合。它擁有List集合的特色:java
- 存取有序
- 帶索引
- 容許重複元素
它自己的特色是:數組
- 查找元素快
- 順序插入快
那ArrayList爲何會有這些特性的?其實從源碼中咱們就可以瞭解到它是如何實現的。安全
Resizable-array implementation of the {@code List} interface. Implements all optional list operations, and permits all elements, including {@code null}. In addition to implementing the {@code List} interface,this class provides methods to manipulate the size of the array that is used internally to store the list.數據結構
實現了List接口的,一個可調整大小的數組。能夠存儲全部的元素,包括null,除了實現了List接口的方法外,還提供了一些方法用於操做內部用於存儲元素的數組的大小。多線程
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
dom
- RandomAccess標記接口 表示ArrayList支持隨機訪問。什麼是隨機訪問呢?隨機訪問是說你能夠隨意訪問該數據結構中的任意一個節點,假設該數據結構有10000個節點,你能夠隨意訪問第1個到第10000個節點。由於ArrayList用數組來存數據,Java中給數組劃份內存來存儲元素時,劃分的是一塊連續的內存地址,這些相鄰元素是按順序連續存儲的。只要知道起始元素的地址First,直接first+N,即可以獲得第N個元素的地址。這就是ArrayList查找快的緣由。
- Cloneable標記接口 表示ArrayList是能夠被克隆的。
- java.io.Serializable標記接口 表示ArrayList是能夠被序列化的。
主要成員變量:ide
@java.io.Serial private static final long serialVersionUID = 8683452581122892189L;
性能
用於標明序列化時的版本號。this
private static final int DEFAULT_CAPACITY = 10;
.net
默認初始化大小。
private static final Object[] EMPTY_ELEMENTDATA = {};
默認空數組大小。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
默認初始化空數組大小。
看到這可能有人會有疑問,一樣是空數組,爲何要有兩個引用變量來表示呢?且看後面的擴容機制說明。
transient Object[] elementData;
這就是ArrayList所維護的用於存儲數據的數組了,transient標明這個存儲數據的數組不會被序列化,而ArrayList卻又打上了標記接口java.io.Serializable說明是可序列化的,這不是自相矛盾了嗎?暫且按下不表,看看ArrayList的內部機制再回頭說明。
private int size;
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; } //傳入一個Collection集合的 public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
能夠看到,若是傳入了初始值initialCapacity,就會按照這個值來初始化數組的大小。若是傳入0,則數組等於EMPTY_ELEMENTDATA,若是用了空參構造,則數組等於DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
擴容機制:
咱們知道,Java中數組一旦定義則長度是不容許改變的,那麼ArrayList如何實現_Resizable-array implementation(可調整大小的數組實現)?_
答案就在擴容機制上:
//傳入最小所須要的容量 private Object[] grow(int minCapacity) { //當前容量大小 int oldCapacity = elementData.length; //若當前容量大小大於0且數組建立是否是經過空參構造建立的 if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //定義新的最小所需容量 int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ //右移一位,相似於除以2 oldCapacity >> 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } } //傳入當前容量,最小須要增長的容量,首選增加的容量 public static int newLength(int oldLength, int minGrowth, int prefGrowth) { // assert oldLength >= 0 // assert minGrowth > 0 //將最小所須要增長的容量與首選增加的容量比較,取較大的與當前容量相加 int newLength = Math.max(minGrowth, prefGrowth) + oldLength; if (newLength - MAX_ARRAY_LENGTH <= 0) { return newLength; } return hugeLength(oldLength, minGrowth); }
咱們每次添加元素的時候,都會去確認一下當前ArrayList所維護的數組存儲的元素是否已經達到上限(元素個數等於數組長度)?若是是的話,就會觸發擴容機制grow()去建立一個新的更大的數組來轉移數據。
能夠看到,如果咱們經過傳入參數0來構造ArrayList,grow()就會判斷出來這個是一個長度爲0的自定義空數組,那麼就會按照最小的所須要的容量擴容。
若是沒有傳入參數,就用默認初始容量10,來建立初始的數組。
並且咱們能夠看到,擴容的時候會判斷所須要的最小容量是否是比當前數組的1.5倍還大?不是就按照當前數組長度的1.5倍來擴容,不然就按傳進來的最小所需容量來擴容。
爲何是1.5倍呢?
1.若是一次性擴容擴得太大,必然形成內存空間的浪費。
2.若是一次性擴容擴得不夠,那麼下一次擴容的操做必然比較快地會到來,這會下降程序運行效率,要知道擴容仍是比較耗費性能的一個操做,由於會新建數組移動數據。
因此擴容擴多少,是JDK開發人員在時間、空間上作的一個權衡,提供出來的一個比較合理的數值。
添加元素:
//添加單個元素,默認添加到集合尾 public boolean add(E e) { modCount++; add(e, elementData, size); return true; } //實際調用的方法 private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; } //添加另外一個集合,默認添加到集合尾 public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); modCount++; int numNew = a.length; if (numNew == 0) return false; Object[] elementData; final int s; if (numNew > (elementData = this.elementData).length - (s = size)) elementData = grow(s + numNew); //5個參數的意思分別是:源數組,源數組開始複製的位置,目標數組,目標數組開始接收的位置,接收的長度 System.arraycopy(a, 0, elementData, s, numNew); size = s + numNew; return true; }
基本上很淺顯易懂,只要容量夠就能直接添加,不然得擴容。
其中modCount是用於快速失敗的一種機制,由於基本集合在多線程環境下是不安全的,這個之後再討論。
刪除元素:
//按元素刪除 public boolean remove(Object o) { final Object[] es = elementData; final int size = this.size; int i = 0; //標籤舊方法,用於跳出多個循環 found: { if (o == null) { for (; i < size; i++) if (es[i] == null) break found; } else { for (; i < size; i++) if (o.equals(es[i])) break found; } return false; } fastRemove(es, i); return true; } //按索引刪除 public E remove(int index) { Objects.checkIndex(index, size); final Object[] es = elementData; @SuppressWarnings("unchecked") E oldValue = (E) es[index]; fastRemove(es, index); return oldValue; } //實際執行方法 private void fastRemove(Object[] es, int i) { modCount++; final int newSize; if ((newSize = size - 1) > i) System.arraycopy(es, i + 1, es, i, newSize - i); es[size = newSize] = null; }
分兩種狀況,一種是按照傳入的元素去刪除,這樣得從數組開頭遍歷過去而且逐一調用equals方法比較,只要找到第一個符合的就將其刪除。
第二種是按照傳入的索引去刪除,這個能夠直接定位。另外須要注意的一點是當ArrayList裏存儲的是Integer包裝類的時候,依然是選擇索引刪除。
刪除的意思是將數組此位置的元素的引用設置爲null,若是沒有另外的引用指向此元素,那麼此元素就會被標記爲垃圾被回收。
若是刪除的元素在最後就不用移動元素了,若是不在就須要移動元素。
插入元素:
public void add(int index, E element) { rangeCheckForAdd(index); modCount++; final int s; Object[] elementData; if ((s = size) == (elementData = this.elementData).length) elementData = grow(); System.arraycopy(elementData, index, elementData, index + 1, s - index); elementData[index] = element; size = s + 1; }
先判斷容量是否足夠,再去將此位置以及後面的元素所有向後移動一位,最後將傳入的元素插入此位置。
最後說明:
瞭解了ArrayList的各類機制,咱們就能知道爲何存儲元素的elementData用transient修飾了
//序列化 @java.io.Serial 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 behavioral 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(); } } //反序列化 @java.io.Serial private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in size, and any hidden stuff s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if (size > 0) { // like clone(), allocate array based upon size not capacity SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size); Object[] elements = new Object[size]; // Read in all elements in the proper order. for (int i = 0; i < size; i++) { elements[i] = s.readObject(); } elementData = elements; } else if (size == 0) { elementData = EMPTY_ELEMENTDATA; } else { throw new java.io.InvalidObjectException("Invalid size: " + size); } }
其實主要是看裏邊的兩個循環的方法,涉及到的是size。咱們已經瞭解了存放元素的數組會動態的改變,所以裏邊未必就存滿了元素。真正有多少個元素是size負責的。因此咱們序列化的時候僅僅去遍歷size個對象就能完成序列化了。這樣就能避免序列化太多不須要的東西,加快序列化速度以及減少文件大小。
總結: 在須要順序添加數據以及快速訪問數組的場景,ArrayList最適合被使用。