本篇分析ArrayList的源碼,在分析之前先跟大家談一談數組。數組可能是我們最早接觸到的數據結構之一,它是在內存中划分出一塊連續的地址空間用來進行元素的存儲,由於它直接操作內存,所以數組的性能要比集合類更好一些,這是使用數組的一大優勢。但是我們知道數組存在致命的缺陷,就是在初始化時必須指定數組大小,並且在后續操作中不能再更改數組的大小。在實際情況中我們遇到更多的是一開始並不知道要存放多少元素,而是希望容器能夠自動的擴展它自身的容量以便能夠存放更多的元素。ArrayList就能夠很好的滿足這樣的需求,它能夠自動擴展大小以適應存儲元素的不斷增加。它的底層是基於數組實現的,因此它具有數組的一些特點,例如查找修改快而插入刪除慢。本篇我們將深入源碼看看它是怎樣對數組進行封裝的。首先看看它的成員變量和三個主要的構造器。
1 //默認初始化容量 2 private static final int DEFAULT_CAPACITY = 10; 3 4 //空對象數組 5 private static final Object[] EMPTY_ELEMENTDATA = {}; 6 7 //對象數組 8 private transient Object[] elementData; 9 10 //集合元素個數 11 private int size; 12 13 //傳入初始容量的構造方法 14 public ArrayList(int initialCapacity) { 15 super(); 16 if (initialCapacity < 0) { 17 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); 18 } 19 //新建指定容量的Object類型數組 20 this.elementData = new Object[initialCapacity]; 21 } 22 23 //不帶參數的構造方法 24 public ArrayList() { 25 super(); 26 //將空的數組實例傳給elementData 27 this.elementData = EMPTY_ELEMENTDATA; 28 } 29 30 //傳入外部集合的構造方法 31 public ArrayList(Collection<? extends E> c) { 32 //持有傳入集合的內部數組的引用 33 elementData = c.toArray(); 34 //更新集合元素個數大小 35 size = elementData.length; 36 //判斷引用的數組類型, 並將引用轉換成Object數組引用 37 if (elementData.getClass() != Object[].class) { 38 elementData = Arrays.copyOf(elementData, size, Object[].class); 39 } 40 }
可以看到ArrayList的內部存儲結構就是一個Object類型的數組,因此它可以存放任意類型的元素。在構造ArrayList的時候,如果傳入初始大小那么它將新建一個指定容量的Object數組,如果不設置初始大小那么它將不會分配內存空間而是使用空的對象數組,在實際要放入元素時再進行內存分配。下面再看看它的增刪改查方法。
1 //增(添加) 2 public boolean add(E e) { 3 //添加前先檢查是否需要拓展數組, 此時數組長度最小為size+1 4 ensureCapacityInternal(size + 1); 5 //將元素添加到數組末尾 6 elementData[size++] = e; 7 return true; 8 } 9 10 11 //增(插入) 12 public void add(int index, E element) { 13 //插入位置范圍檢查 14 rangeCheckForAdd(index); 15 //檢查是否需要擴容 16 ensureCapacityInternal(size + 1); 17 //挪動插入位置后面的元素 18 System.arraycopy(elementData, index, elementData, index + 1, size - index); 19 //在要插入的位置賦上新值 20 elementData[index] = element; 21 size++; 22 } 23 24 //刪 25 public E remove(int index) { 26 //index不能大於size 27 rangeCheck(index); 28 modCount++; 29 E oldValue = elementData(index); 30 int numMoved = size - index - 1; 31 if (numMoved > 0) { 32 //將index后面的元素向前挪動一位 33 System.arraycopy(elementData, index+1, elementData, index, numMoved); 34 } 35 //置空引用 36 elementData[--size] = null; 37 return oldValue; 38 } 39 40 //改 41 public E set(int index, E element) { 42 //index不能大於size 43 rangeCheck(index); 44 E oldValue = elementData(index); 45 //替換成新元素 46 elementData[index] = element; 47 return oldValue; 48 } 49 50 //查 51 public E get(int index) { 52 //index不能大於size 53 rangeCheck(index); 54 //返回指定位置元素 55 return elementData(index); 56 }
每次添加一個元素到集合中都會先檢查容量是否足夠,否則就進行擴容,擴容的細節下面會講到。我們先看具體增刪改查要注意的地方。
增(添加):僅是將這個元素添加到末尾。操作快速。
增(插入):由於需要移動插入位置后面的元素,並且涉及數組的復制,所以操作較慢。
刪:由於需要將刪除位置后面的元素向前挪動,也會設計數組復制,所以操作較慢。
改:直接對指定位置元素進行修改,不涉及元素挪動和數組復制,操作快速。
查:直接返回指定下標的數組元素,操作快速。
通過源碼看到,由於查找和修改直接定位到數組下標,不涉及元素挪動和數組復制所以較快,而插入刪除由於要挪動元素,涉及到數組復制,操作較慢。並且每次添加操作還可能進行數組擴容,也會影響到性能。下面我們看看ArrayList是怎樣動態擴容的。
1 private void ensureCapacityInternal(int minCapacity) { 2 //如果此時還是空數組 3 if (elementData == EMPTY_ELEMENTDATA) { 4 //和默認容量比較, 取較大值 5 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); 6 } 7 //數組已經初始化過就執行這一步 8 ensureExplicitCapacity(minCapacity); 9 } 10 11 private void ensureExplicitCapacity(int minCapacity) { 12 modCount++; 13 //如果最小容量大於數組長度就擴增數組 14 if (minCapacity - elementData.length > 0) { 15 grow(minCapacity); 16 } 17 } 18 19 //集合最大容量 20 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 21 22 //增加數組長度 23 private void grow(int minCapacity) { 24 //獲取數組原先的容量 25 int oldCapacity = elementData.length; 26 //新數組的容量, 在原來的基礎上增加一半 27 int newCapacity = oldCapacity + (oldCapacity >> 1); 28 //檢驗新的容量是否小於最小容量 29 if (newCapacity - minCapacity < 0) { 30 newCapacity = minCapacity; 31 } 32 //檢驗新的容量是否超過最大數組容量 33 if (newCapacity - MAX_ARRAY_SIZE > 0) { 34 newCapacity = hugeCapacity(minCapacity); 35 } 36 //拷貝原來的數組到新數組 37 elementData = Arrays.copyOf(elementData, newCapacity); 38 }
每次添加元素前會調用ensureCapacityInternal
這個方法進行集合容量檢查。在這個方法內部會檢查當前集合的內部數組是否還是個空數組,如果是就新建默認大小為10的Object數組。如果不是則證明當前集合已經被初始化過,那么就調用ensureExplicitCapacity方法檢查當前數組的容量是否滿足這個最小所需容量,不滿足的話就調用grow方法進行擴容。在grow方法內部可以看到,每次擴容都是增加原來數組長度的一半,擴容實際上是新建一個容量更大的數組,將原先數組的元素全部復制到新的數組上,然后再拋棄原先的數組轉而使用新的數組。
至此,我們對ArrayList中比較常用的方法做了分析,其中有些值得注意的要點:
1. ArrayList底層實現是基於數組的,因此對指定下標的查找和修改比較快,但是刪除和插入操作比較慢。
2. 構造ArrayList時盡量指定容量,減少擴容時帶來的數組復制操作,如果不知道大小可以賦值為默認容量10。
3. 每次添加元素之前會檢查是否需要擴容,每次擴容都是增加原有容量的一半。
4. 每次對下標的操作都會進行安全性檢查,如果出現數組越界就立即拋出異常。
5. ArrayList的所有方法都沒有進行同步,因此它不是線程安全的。
6. 以上分析基於JDK1.7,其他版本會有些出入,因此不能一概而論。