java提高篇(二一)-----ArrayList
一、ArrayList概述
ArrayList是實現List接口的動態數組,所謂動態就是它的大小是可變的。實現了所有可選列表操作,並允許包括 null 在內的所有元素。除了實現 List 接口外,此類還提供一些方法來操作內部用來存儲列表的數組的大小。
每個ArrayList實例都有一個容量,該容量是指用來存儲列表元素的數組的大小。默認初始容量爲10。隨着ArrayList中元素的增加,它的容量也會不斷的自動增長。在每次添加新的元素時,ArrayList都會檢查是否需要進行擴容操作,擴容操作帶來數據向新數組的重新拷貝,所以如果我們知道具體業務數據量,在構造ArrayList時可以給ArrayList指定一個初始容量,這樣就會減少擴容時數據的拷貝問題。當然在添加大量元素前,應用程序也可以使用ensureCapacity操作來增加ArrayList實例的容量,這可以減少遞增式再分配的數量。
注意,ArrayList實現不是同步的。如果多個線程同時訪問一個ArrayList實例,而其中至少一個線程從結構上修改了列表,那麼它必須保持外部同步。所以爲了保證同步,最好的辦法是在創建時完成,以防止意外對列表進行不同步的訪問:
List list = Collections.synchronizedList(new ArrayList(...));
二、ArrayList源碼分析
ArrayList我們使用的實在是太多了,非常熟悉,所以在這裏將不介紹它的使用方法。ArrayList是實現List接口的,底層採用數組實現,所以它的操作基本上都是基於對數組的操作。
2.1、底層使用數組
private transient Object[] elementData;
transient??爲java關鍵字,爲變量修飾符,如果用transient聲明一個實例變量,當對象存儲時,它的值不需要維持。Java的serialization提供了一種持久化對象實例的機制。當持久化對象時,可能有一個特殊的對象數據成員,我們不想用serialization機制來保存它。爲了在一個特定對象的一個域上關閉serialization,可以在這個域前加上關鍵字transient。當一個對象被序列化的時候,transient型變量的值不包括在序列化的表示中,然而非transient型的變量是被包括進去的。
這裏Object[] elementData,就是我們的ArrayList容器,下面介紹的基本操作都是基於該elementData變量來進行操作的。
2.2、構造函數
ArrayList提供了三個構造函數:
ArrayList():默認構造函數,提供初始容量爲10的空列表。
ArrayList(int initialCapacity):構造一個具有指定初始容量的空列表。
ArrayList(Collection<? extends E> c):構造一個包含指定 collection 的元素的列表,這些元素是按照該 collection 的迭代器返回它們的順序排列的。
/** * 構造一個初始容量爲 10 的空列表 */ public ArrayList() { this(10); } /** * 構造一個具有指定初始容量的空列表。 */ public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); this.elementData = new Object[initialCapacity]; } /** * 構造一個包含指定 collection 的元素的列表,這些元素是按照該 collection 的迭代器返回它們的順序排列的。 */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); }
2.3、新增
ArrayList提供了add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)、set(int index, E element)這個五個方法來實現ArrayList增加。
add(E e):將指定的元素添加到此列表的尾部。
public boolean add(E e) { ensureCapacity(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
這裏ensureCapacity()方法是對ArrayList集合進行擴容操作,elementData(size++) = e,將列表末尾元素指向e。
add(int index, E element):將指定的元素插入此列表中的指定位置。
public void add(int index, E element) { //判斷索引位置是否正確 if (index > size || index < 0) throw new IndexOutOfBoundsException( "Index: "+index+", Size: "+size); //擴容檢測 ensureCapacity(size+1); /* * 對源數組進行復制處理(位移),從index + 1到size-index。 * 主要目的就是空出index位置供數據插入, * 即向右移動當前位於該位置的元素以及所有後續元素。 */ System.arraycopy(elementData, index, elementData, index + 1, size - index); //在指定位置賦值 elementData[index] = element; size++; }
在這個方法中最根本的方法就是System.arraycopy()方法,該方法的根本目的就是將index位置空出來以供新數據插入,這裏需要進行數組數據的右移,這是非常麻煩和耗時的,所以如果指定的數據集合需要進行大量插入(中間插入)操作,推薦使用LinkedList。
addAll(Collection<? extends E> c):按照指定 collection 的迭代器所返回的元素順序,將該 collection 中的所有元素添加到此列表的尾部。
public boolean addAll(Collection<? extends E> c) { // 將集合C轉換成數組 Object[] a = c.toArray(); int numNew = a.length; // 擴容處理,大小爲size + numNew ensureCapacity(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; }
這個方法無非就是使用System.arraycopy()方法將C集合(先準換爲數組)裏面的數據複製到elementData數組中。這裏就稍微介紹下System.arraycopy(),因爲下面還將大量用到該方法。該方法的原型爲:public static voidarraycopy(Object src, int srcPos, Object dest, int destPos, int length)。它的根本目的就是進行數組元素的複製。即從指定源數組中複製一個數組,複製從指定的位置開始,到目標數組的指定位置結束。將源數組src從srcPos位置開始複製到dest數組中,複製長度爲length,數據從dest的destPos位置開始粘貼。
addAll(int index, Collection<? extends E> c):從指定的位置開始,將指定 collection 中的所有元素插入到此列表中。
public boolean addAll(int index, Collection<? extends E> c) { //判斷位置是否正確 if (index > size || index < 0) throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); //轉換成數組 Object[] a = c.toArray(); int numNew = a.length; //ArrayList容器擴容處理 ensureCapacity(size + numNew); // Increments modCount //ArrayList容器數組向右移動的位置 int numMoved = size - index; //如果移動位置大於0,則將ArrayList容器的數據向右移動numMoved個位置,確保增加的數據能夠增加 if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); //添加數組 System.arraycopy(a, 0, elementData, index, numNew); //容器容量變大 size += numNew; return numNew != 0; }
set(int index, E element):用指定的元素替代此列表中指定位置上的元素。
public E set(int index, E element) { //檢測插入的位置是否越界 RangeCheck(index); E oldValue = (E) elementData[index]; //替代 elementData[index] = element; return oldValue; }
2.4、刪除
ArrayList提供了remove(int index)、remove(Object o)、removeRange(int fromIndex, int toIndex)、removeAll()四個方法進行元素的刪除。
remove(int index):移除此列表中指定位置上的元素。
public E remove(int index) { //位置驗證 RangeCheck(index); modCount++; //需要刪除的元素 E oldValue = (E) elementData[index]; //向左移的位數 int numMoved = size - index - 1; //若需要移動,則想左移動numMoved位 if (numMoved > 0) System.arraycopy(elementData, index + 1, elementData, index, numMoved); //置空最後一個元素 elementData[--size] = null; // Let gc do its work return oldValue; }
remove(Object o):移除此列表中首次出現的指定元素(如果存在)。
public boolean remove(Object o) { //因爲ArrayList中允許存在null,所以需要進行null判斷 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; }
其中fastRemove()方法用於移除指定位置的元素。如下
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; // Let gc do its work }
removeRange(int fromIndex, int toIndex):移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之間的所有元素。
protected void removeRange(int fromIndex, int toIndex) { modCount++; int numMoved = size - toIndex; System .arraycopy(elementData, toIndex, elementData, fromIndex, numMoved); // Let gc do its work int newSize = size - (toIndex - fromIndex); while (size != newSize) elementData[--size] = null; }
removeAll():是繼承自AbstractCollection的方法,ArrayList本身並沒有提供實現。
public boolean removeAll(Collection<?> c) { boolean modified = false; Iterator<?> e = iterator(); while (e.hasNext()) { if (c.contains(e.next())) { e.remove(); modified = true; } } return modified; }
2.5、查找
ArrayList提供了get(int index)用讀取ArrayList中的元素。由於ArrayList是動態數組,所以我們完全可以根據下標來獲取ArrayList中的元素,而且速度還比較快,故ArrayList長於隨機訪問。
public E get(int index) { RangeCheck(index); return (E) elementData[index]; }
2.6、擴容
在上面的新增方法的源碼中我們發現每個方法中都存在這個方法:ensureCapacity(),該方法就是ArrayList的擴容方法。在前面就提過ArrayList每次新增元素時都會需要進行容量檢測判斷,若新增元素後元素的個數會超過ArrayList的容量,就會進行擴容操作來滿足新增元素的需求。所以當我們清楚知道業務數據量或者需要插入大量元素前,我可以使用ensureCapacity來手動增加ArrayList實例的容量,以減少遞增式再分配的數量。
public void ensureCapacity(int minCapacity) { //修改計時器 modCount++; //ArrayList容量大小 int oldCapacity = elementData.length; /* * 若當前需要的長度大於當前數組的長度時,進行擴容操作 */ if (minCapacity > oldCapacity) { Object oldData[] = elementData; //計算新的容量大小,爲當前容量的1.5倍 int newCapacity = (oldCapacity * 3) / 2 + 1; if (newCapacity < minCapacity) newCapacity = minCapacity; //數組拷貝,生成新的數組 elementData = Arrays.copyOf(elementData, newCapacity); } }
在這裏有一個疑問,爲什麼每次擴容處理會是1.5倍,而不是2.5、3、4倍呢?通過google查找,發現1.5倍的擴容是最好的倍數。因爲一次性擴容太大(例如2.5倍)可能會浪費更多的內存(1.5倍最多浪費33%,而2.5被最多會浪費60%,3.5倍則會浪費71%……)。但是一次性擴容太小,需要多次對數組重新分配內存,對性能消耗比較嚴重。所以1.5倍剛剛好,既能滿足性能需求,也不會造成很大的內存消耗。
處理這個ensureCapacity()這個擴容數組外,ArrayList還給我們提供了將底層數組的容量調整爲當前列表保存的實際元素的大小的功能。它可以通過trimToSize()方法來實現。該方法可以最小化ArrayList實例的存儲量。
public void trimToSize() { modCount++; int oldCapacity = elementData.length; if (size < oldCapacity) { elementData = Arrays.copyOf(elementData, size); } }
出處: http://www.cnblogs.com/chenssy/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。