ArrayList的實現細節(基於JDK1.8)
ArrayList是我們經常用到的一個類,下面總結一下它內部的實現細節和使用時要註意的地方。
基本概念
ArrayList在數據結構的層面上講,是一個用數組實現的list,從應用層面上講,就是一個容量會自己改變的數組,具有一系列方便的add、set、get、remove等方法,線程不安全。先上張類圖吧。
ArrayList的容量
ArrayList有兩個數據域與之相關。
1 transient Object[] elementData; // non-private to simplify nested class access 2 3 private int size;
很明顯,size表示ArrayList中包含的元素數量,也就是size()方法的返回值,而elementData.length則是ArrayList的容量,表示在不擴容的情況下能存儲多少個元素。By the way,JDK1.8的ArrayList的初始容量是0,之前的版本貌似10。
ArrayList還有一些關於擴大容量和縮小容量的方法
1 /** 2 * 當ArrayList中有空閑的空間時,縮減ArrayList的容量。應用程序可以使用這個方法最小化ArrayList實例 3 */ 4 public void trimToSize() 5 6 /** 7 * public修飾,供應用程序調用的擴容方法,內部調用ensureExplicitCapacity()方法 8 */ 9 public void ensureCapacity(int minCapacity) 10 11 /** 12 * private修飾,供ArrayList內部使用的擴容方法,內部同樣是調用ensureExplicitCapacity()方法13 */ 14 private void ensureCapacityInternal(int minCapacity) 15 16 /** 17 * 內部調用grow()方法 18 */ 19 private void ensureExplicitCapacity(int minCapacity) 20 21 /** 22 * grow()方法內部會做一個判斷,如果ArrayList擴大1.5倍還不夠的話,才會增加到minCapacity 23 * 這是為了防止擴容太小而導致多次擴容多次改變數組大小,從而影響性能。24 * 比如說,我有一個裝滿了的ArrayList,現在我往其中加入10個元素,自然是要擴容的, 25 * 那麽我是一次性擴容增加10個容量,還是每次add前擴容增加一個容量呢,答案可想而知。 26 */ 27 private void grow(int minCapacity) 28 29 /** 30 * 對ArrayList擴容的一個限制,擴得太大會拋出OutOfMemoryError 31 */ 32 private static int hugeCapacity(int minCapacity)
雖說的容量會隨著數據量的增大而增大,使用時不用費心於容量的維護,不過在可以預估數據量的情況下,務必使用public ArrayList(int initialCapacity)來指定初始容量,這樣的話,一來減少擴容方法的調用避免數組頻繁更改,二來在一定程度上減少了內存的消耗(比如我就存5000個元素,當數組達到4000時擴容擴大1.5倍變成6000,白白耗費了1000個單位的內存)。經過測試,這是可以大大提高運行效率的。
Clone
ArrayList的clone()方法是淺復制,在這裏直接上段demo。
1 public class Main { 2 public static void main(String[] args) { 3 User u1 = new User(); 4 u1.setUsername("qwe"); 5 u1.setPassword("qwePASSWORD"); 6 User u2 = new User(); 7 u2.setUsername("asd"); 8 u2.setPassword("asdPassword"); 9 ArrayList<User> list1 = new ArrayList<>(); 10 list1.add(u1); 11 list1.add(u2); 12 ArrayList<User> list2 = (ArrayList<User>) list1.clone(); 13 list2.get(0).setUsername("zxc"); //修改u1的username 14 list2.get(0).setPassword("zxcPassword"); ////修改u1的password 15 System.out.println(list1); //[User [username=zxc, password=zxcPassword], User //[username=asd, password=asdPassword]] 16 } 17 /** 18 * 實現深復制 19 */ 20 private static List<User> deepClone(List<User> from) throws CloneNotSupportedException { 21 List<User> list = new ArrayList<>(); 22 for(User item : from) { 23 list.add((User)item.clone()); 24 } 25 return list; 26 } 27 } 28 29 class User { 30 private String username; 31 private String password; 32 public User() { 33 } 34 public String getUsername() { 35 return username; 36 } 37 public void setUsername(String username) { 38 this.username = username; 39 } 40 public String getPassword() { 41 return password; 42 } 43 public void setPassword(String password) { 44 this.password = password; 45 } 46 @Override 47 public String toString() { 48 return "User [username=" + username + ", password=" + password + "]"; 49 } 50 }
有輸出可知,list2中的u1就是list1中的u1,二者的引用指向了同一個User對象,具體見示意圖。所以要想實現ArrayList的深復制得根據場景自己寫。
public Object[] toArray()和public T[] toArray(T[] a)
1 /** 2 * 獲得一個Object數組,這個方法會分配一個新數組(並不是單純的return elementData;),所以調用者可以安全的修改數組而不影響ArrayList 3 */ 4 public Object[] toArray() 5 6 /** 7 * 獲得一個泛型數組 8 */ 9 public <T> T[] toArray(T[] a) { 10 if (a.length < size) //數組a長度不足,則重新new一個數組 11 // Make a new array of a‘s runtime type, but my contents: 12 return (T[]) Arrays.copyOf(elementData, size, a.getClass()); 13 System.arraycopy(elementData, 0, a, 0, size); //數組a長度足夠,就將元素復制到a數組中,而後返回a 14 if (a.length > size) 15 a[size] = null; 16 return a; 17 }
This method acts as bridge between array-based and collection-based APIs.這是文檔註釋中的一句話,大意是這個方法是數組和集合之間的橋梁。通過函數簽名,我們可以得知toArray()返回一個Object數組,toArray(T[] a)返回一個泛型數組。我們往往使用的是toArray(T[] a),常見的使用方式如下
1 List<Integer> list = new ArrayList<>(); 2 Collections.addAll(list,1,2,3,4,5,6); 3 // 方式1 // 4 list.toArray(new Integer[0]); //涉及到反射,效率較低 5 // 方式2 // 6 list.toArray(new Integer[list.size()])
構造函數:public ArrayList(Collection c)
1 public ArrayList(Collection<? extends E> c) { 2 elementData = c.toArray(); 3 if ((size = elementData.length) != 0) { 4 // c.toArray might (incorrectly) not return Object[] (see 6260652) 5 if (elementData.getClass() != Object[].class) 6 elementData = Arrays.copyOf(elementData, size, Object[].class); 7 } else { 8 // replace with empty array. 9 this.elementData = EMPTY_ELEMENTDATA; 10 } 11 }
利用這個構造方法,我們可以方便的使用其他容器來構造一個ArrayList。這裏有一個要點,通過源碼我們得知,當elementData不是Object數組時,它會使用Arrays.copyOf()方法構造一個Object數組替換elementData,為什麽要這麽做呢,Object[] objArr = new String[5];之類的代碼完全不會報錯啊。我們先看一段代碼,理解Java數組的一個特性,Java數組要求其存儲的元素必須是new數組時的實際類型的實例。
1 Object[] objArr = new String[5]; 2 objArr[0] = "qwe"; 3 objArr[1] = new Object(); //java.lang.ArrayStoreException 4 System.out.println(Arrays.toString(objArr));
數組objArr的實際類型是String數組,所以它只能存儲Stirng類型的對象實例(String沒有子類),不然就拋出異常。
理解了ArrayStoreException,我們再回到ArrayList。假設在使用上面那個構造函數時,不轉換成Object數組類型,當我們使用toArray()方法時就會出問題了,正如註釋所說:c.toArray
might (incorrectly) not return
Object[]。使用toArray()方法獲得一個Object數組,直觀意思就是可以往裏面加任何類型的實例啊,但是如果不在上面那個構造函數中特殊處理,是會拋java.lang.ArrayStoreException。這就是為什麽ArrayList要對非Object數組特殊處理:為了toArray()返回的Object數組能夠正常使用。
1 List<String> list = new ArrayList<String>(); 2 list.add("one"); 3 list.add("two"); 4 Object[] arr = list.toArray(); //這個arr數組可以正常使用,真是nice啊 5 // class [Ljava.lang.Object;返回的是Object數組 6 System.out.println(arr.getClass()); 7 arr[0] = ""; 8 arr[0] = 123; 9 arr[0] = new Object();
fail-fast:快速失敗
fail-fast是指在多線程環境下,比如一個線程在讀(這裏僅考慮叠代器叠代),一個線程在寫的情況下容易出現匪夷所思的bug,為了更好的調試,采用了快速失敗機制,一旦發現異步修改,馬上拋異常而不是繼續叠代下去。當然,ArrayListd的實現更加嚴格,在單線程環境下作死的話也會拋出異常。
1 List<Integer> list = new ArrayList<Integer>(); 2 Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7); 3 Iterator<Integer> iterator = list.iterator(); 4 list.add(8); //修改了ArrayList 5 while(iterator.hasNext()) { 6 System.out.println(iterator.next()); //java.util.ConcurrentModificationException 7 }
下面再簡單講幾句ArrayList實現快速失敗的機制。ArrayList的快速失敗是圍繞著叠代器的,所以定位到叠代器的源碼。獲得一個叠代器後, expectedModCount值就確定了,可是modCount可能會改變(trimToSize()、ensureExplicitCapacity()、remove()、clear()等等都會修改modCount)。往後使用叠代器的過程中,一旦expectedModCount不等於modCount,就認為叠代的結果有問題,不管三七二十一就拋出ConcurrentModificationException。
1 private class Itr implements Iterator<E> { 2 /** 3 * 每構造一個叠代器都會記錄當前的modCount,modCount之後有可能會改變 4 */ 5 int expectedModCount = modCount; 6 /** 7 * 當modCount不等於expectedModCount就拋出ConcurrentModificationException 8 */ 9 final void checkForComodification() { 10 if (modCount != expectedModCount) 11 throw new ConcurrentModificationException(); 12 } 13 }
務必理解文檔註釋中的一段話。
1 he iterators returned by this class‘s iterator and listIterator methods are fail-fast: 2 if the list is structurally modified at any time after the iterator is created, in any way except through the iterator‘s own remove or add methods, the iterator will throw a ConcurrentModificationException. 3 Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future. 4 5 快速失敗是指:叠代器被創建後,list發生了結構型的變化(除了使用叠代器自己的add或者remove操作),叠代器使用時會拋出ConcurrentModificationException。 6 該類的iterator和listIterator都是快速失敗的。 7 因此,面對並發修改,叠代器將快速的拋出異常終止叠代,而不是冒著風險在非確定的未來進行非確定性行為。
ArrayList的序列化機制
通過UML圖,我們知道ArrayList實現了Serializable接口,通過源碼,我們又知道ArrayList的序列化機制、反序列化機制是自定義的。
/** * 自定義序列化機制 */ private void writeObject(java.io.ObjectOutputStream s) /** * 自定義反序列化機制 */ private void readObject(java.io.ObjectInputStream s)
那麽為什麽要自定義序列化、反序列化機制呢?是由於ArrayList實質上是一個動態數組,往往數組中會有空余的空間,如果采用默認的序列化機制,那些空余的空間會作為null寫入本地文件或者在網絡中傳輸,耗費了不必要的資源。所以,ArrayList使用自定義序列化機制,僅寫入索引為【0,size)的有效元素以節省資源。
ArrayList的遍歷
ArrayList的遍歷方式有三種:foreach語法糖、普通for循環,叠代器。其中foreach相當於使用叠代器遍歷,而是用叠代器時會有個叠代器對象的開銷,所以一般情況下普通的for循環遍歷效率更高。
1 ArrayList<Integer> list = new ArrayList<>(); 2 Collections.addAll(list,1,2,3,4,5,6,7); 3 int len = list.size(); //避免重復調用list.size()方法 4 for(int i=0;i<len;i++) { 5 System.out.print(list.get(i)); //隨機訪問 6 }
RandomAccess接口
RandomAccess是一個標記接口,用於標記當前類是可以隨機訪問的,有什麽用?我們先看看JDK中一個典型的應用場景。
1 /** 2 * Collections.fill() 3 */ 4 public static <T> void fill(List<? super T> list, T obj) { 5 int size = list.size(); 6 if (size < FILL_THRESHOLD || list instanceof RandomAccess) { 7 for (int i=0; i<size; i++) 8 list.set(i, obj); 9 } else { 10 ListIterator<? super T> itr = list.listIterator(); 11 for (int i=0; i<size; i++) { 12 itr.next(); 13 itr.set(obj); 14 } 15 } 16 }
上面這段代碼,大概的業務邏輯是指當list是RandomAccess的實例時,便用普通的for循環遍歷,如果不是RandomAccess實例時,則用叠代器遍歷。
前面一點已經講了,對於ArrayList,普通的for循環遍歷效率比用叠代器遍歷效率高。現在拓展這一點:當一個類標記了RandomAccess接口,那麽表明該類使用for循環遍歷效率更高,如果沒用RandomAccess標記,則使用叠代器遍歷效率更高。平時我們可以模仿Collections.fill(),使用這個特性寫出更美好的代碼。
另外,如果使用普通的for循環遍歷非RandomAccess的實例,效率是很低的,比如LinkedList(實質是一個雙向鏈表),每次get一個元素都要遍歷半個鏈表,所以要格外註意。
System.arraycopy()方法
記得剛學數據結構時,刪除一個元素,添加一個元素是這麽寫的。
/** * 在第索引{@param i}處插入元素{@param item} */ @Override public void add(int i, T item) { // 參數校驗 // if (i < 0 || i > size) { throw new IllegalArgumentException(String.format("i=%d,無效索引值", i)); } // 插入元素 // for (int p = size; p > i; p--) { // 移動數組 arr[p] = arr[p - 1]; } arr[i] = item; size++; } /** * 刪除索引{@param i}處的元素 */ @Override public T remove(int i) { // 參數校驗 // if (i < 0 || i >= size) { throw new IllegalArgumentException(String.format("i=%d,無效索引值", i)); } // 移除節點 // T item = arr[i]; for (int p = i; p < size - 1; p++) { // 移動數組 arr[p] = arr[p + 1]; } arr[--size] = null; return item; }
不論添加、刪除,因為移動數組,所以得用for循環來移動,而且循環的邊界條件很難掌握很容易寫錯,而ArrayList使用了System.arraycopy()來簡化的這一切。掌握了這個,平時我們也可以使用System.arraycopy()來編寫代碼了!
1 public void add(int index, E element) { 2 rangeCheckForAdd(index); //檢查index有沒有越界 3 ensureCapacityInternal(size + 1); // Increments modCount!! 4 System.arraycopy(elementData, index, elementData, index + 1, 5 size - index); //將elementData位於index之後的元素全部向後移一位 6 elementData[index] = element; 7 size++; 8 } 9 10 public E remove(int index) { 11 rangeCheck(index);//檢查index有沒有越界 12 modCount++; 13 E oldValue = elementData(index); 14 int numMoved = size - index - 1; 15 if (numMoved > 0) 16 System.arraycopy(elementData, index+1, elementData, index, 17 numMoved);//將elementData位於index+1之後的元素全部向前移一位 18 elementData[--size] = null; // clear to let GC do its work 19 return oldValue; 20 }
總結
ArrayList是一個線程不安全的動態數組,使用ensureCapacity()擴容,trimToSize縮減容量。
toArray()的使用
System.arraycopy()的使用
引用
1.http://www.cnblogs.com/skywang12345/p/3308556.html
2.http://blog.csdn.net/jzhf2012/article/details/8540410
3.http://blog.csdn.net/ljcITworld/article/details/52041836
4.http://www.cnblogs.com/dolphin0520/p/3933551.html
5.http://www.cnblogs.com/ITtangtang/p/3948555.html
6.http://www.cnblogs.com/java-zhao/p/5102342.html
7.http://www.tuicool.com/articles/uIBB3q
8.http://blog.csdn.net/gulu_gulu_jp/article/details/51457492
9.http://blog.csdn.net/chenssy/article/details/38373833
10.https://www.zhihu.com/question/19882918
11.http://www.cnblogs.com/vinozly/p/5171227.html
ArrayList的實現細節(基於JDK1.8)