List接口
為方便開發人員進行程序開發,JDK提供了一組主要的數據結構實現,如List,Map,Set。網上有許多優秀的源碼解析,就不再做多余分析。本節主要討論List結構的使用方法和優化技巧。
List是最重要的數據結構之一。常見又是最重要的三種List實現:ArrayList,Vector,LinkedList。三種List均來自AbstratList的實現,而AbstratList直接實現List接口,並擴展自AbstratCollection。
其中ArrayList和Vector使用了shuzu實現。可以認為ArrayList或者Vector封裝了對內部數組的操作。比如向數組中添加、刪除、插入新的元素或者數組的擴展和重定義。對ArrayList或者Vector的操作等價對內部對象數據的操作。
ArrayList和Vector幾乎使用了相同的算法,他們唯一區別可以認為是對多線程的支持。ArrayList沒有仁和一個方法做線程同步,因此不是線程安全。Vector中絕大部分方法都做了線程同步,
是一種線程安全的實現。因為ArrayList和Vector的性能相差無幾。從理論上將沒有實現線程同步的ArrayList要稍好於Vector。但實際表現並不明顯。
LinkedList使用了循環雙向鏈表數據結構。與基於數組的List相比,這是兩種截然不同的實現技術,這也決定了他們將適用於完全不同的工作場景。以下是ArrayList和LinkedList的不同比較。
1.增加元素到列表尾端
在ArrayList中增加元素到隊列尾端的代碼如下:
public boolean add(E e) { ensureCapacityInternal(size + 1); // 確保內部數組有足夠的空間 elementData[size++] = e; // 將元素加入到數組的末尾,完成添加 return true; }
ArrayList中的add()方法的性能取決於grow(),實現如下(基於JDK1.8)
private void grow(int minCapacity) { // overflow-conscious codeint oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 擴容到原來的1.5倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 新數組長度小於最小時,為最小 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); //大於最大時為最大 // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
可以看到,只要ArrayList的當前容量足夠大,add()操作的效率是非常高的。
當ArrayList對容量的需求超過當前數組的大小時,才需要進行擴容。擴容過程中會進行大量的數組復制操作。而數組復制時,最終將調用System.arraycopy()方法,因此add()操作的效率還是相當高的。
LinkedList的add()操作實現如下,他也將任意元素增加到隊列的尾端:
public boolean add(E e) { linkLast(e); // 將元素添加直尾端 return true; }
我們再深入linkLast()方法:
void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
可見,LinkedList由於使用了鏈表的結構,因為不需要維護容量的大小。從這點上說,它比ArrayList有一定的性能優勢,然而每次元素增加都需要新建一個Node對象,並進行更多的賦值操作。在頻繁的系統調用中,對性能產生一定的影響。
分別使用ArrayList和LinkedList運行一下代碼(-Xmx512M-Xms512M):
Object obj = new Object(); for(int i = 0; i < 500000; i++) { // 循環50萬次 list.add(obj); }
使用-Xmx512M Xms512M的目的是屏蔽GC對程序執行 速度測量的幹擾(穩定的堆空間會減少GC次數,但也會增加每次GC時長)。ArrayList相對耗時16ms,而LinkedList相對耗時31ms。可見,不間斷地生成新的對象還是占用了一定的系統資源。
2.增加元素到列表任意位置
除了提供了增加元素到List的尾端,List接口還提供了在任意位置插入元素的方法:
void add(int index, E element);
由於實現上的不同,ArrayList和LinkedList在這個方法上存在一定的性能差異。由於ArrayList是基於數組實現,而數組是一塊連續的內存空間,如果在數組的任意位置插入元素,必然導致在該位置後的所有元素需要重新排列,因為效率相對比較低。代碼實現如下:
public void add(int index, E element) { rangeCheckForAdd(index); // 檢測是否存在越界風險 ensureCapacityInternal(size + 1); // 增加長度 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
可以看到,每次插入操作,都會進行一次數組復制。而這個操作在增加元素到List尾端的時候是不存在的。大量的數組重組操作會導致系統性能底下。並且插入的元素在List中的位置越靠前,數組的重組開銷也越大。
而LinkedList此時就顯示出了優勢:
public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); }
可見,對LinkedList來說,在List尾端插入數據與在任意位置插入數據是一樣的。並不會因為插入的位置靠前而導致插入方法的性能降低。如果在系統應用中,List對象需要經常在任意位置插入元素,則可以考慮用LinkedList代替ArrayList以提高系統性能。
3.刪除任意位置元素
對於元素刪除,List接口提供了在任意位置刪除元素的方法:
E remove(int index);
對於ArrayList來說,remove()方法和ad()是雷同的,在任意位置移除元素後,都要進行數組的重組,ArrayList的實現如下:
public E remove(int index) { rangeCheck(index); // 是否存在越界風險 modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) // 將刪除位置後面的元素向前移動以為 System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // 最後一個位置設置為null return oldValue; // 返回刪除元素 }
可以看到,在ArrayList的每一次有效元素刪除操作後,都要進行數組的重組。並且刪除的位置越靠前開銷越大。
public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { // 要刪除的元素位於前半段 Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { // 要刪除的元素位於後半段 Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
在LinkedList的實現中,首先要通過循環找到要刪除的位置處於List的前半段,則從前往後找;如其處於後半段,則從後往前找。因此,無論刪除靠前還是靠後的元素都非常高效。但要刪除較中間的元素幾乎要遍歷完半個List,在List擁有大量元素的情況下,效率很低。下面是測試代碼,所有數據均以一個擁有10萬個元素的List上進行,分別對ArrayList和LinkedList從LIst的頭部、中間和尾部進行刪除。
從頭部刪除元素代碼:
while(list.size() > 0) { list.remove(0); }
從List中間刪除元素代碼:
while(list.size() > 0) { list.remove(list.size() >> 1); }
從List尾部刪除元素:
while(list.size() > 0) { list.remove(list.size() - 1); }
從如下表中可以看到,對於ArrayList從尾部刪除元素是效率很高,符合上文分析,從頭部刪除元素時相當費時;而LinkedList從頭尾刪除元素時效率相差無幾,但是從List中間刪除元素時性能非常糟糕。
List類型/刪除位置 | 頭部 | 中間 | 尾部 |
ArrayList | 6203 | 3125 | 16 |
LinkedList | 15 | 8781 | 16 |
4.容量參數
容量參數是ArrayList和Vector等基於數組的List的特有性能參數,它表示初始化的數組大小。由上文分析可知,當ArrayList所存儲的元素數量超過其已有大小時,它便會進行擴容,數組的擴容會導致整個數組進行一次內存復制。因此合理的數組大小有助於減少數組擴容的次數,從而提高系統性能。
ArrayList默認大小為10,每次擴容後數組大小是原來大小的1.5倍。ArrayList也提供了一個可以指定初始數組大小的構造函數。此處未貼上源碼,如有需要可自行去看兩處構造函數。現以構造一個擁有100萬元素的List為例。當使用默認初始大小時,消耗的相對時間為125ms左右,當直接指定其數組大小為100萬時,構造相同的ArrayList僅相對耗時16ms。
若指定JVM參數-Xms512M-Xms512M,再進行相同的測試。使用默認大小耗時47ms,指定ArrayList初始容量為100萬時,相對耗時16ms。可見通過提升堆內存大小,減少使用初始容量大小時的GC次數,ArrayList擴容時的數組復制,依然占用了較多的CPU時間。
因為,能有效的評估ArrayList的數組大小初始值的情況下,指定容量大小對其性能有較大的提升。
5.遍歷列表
遍歷列表操作也是常用的列表操作之一。在JDK1.5之後,至少有三種方式:ForEach操作、叠代器和for循環。以下代碼實現了三種遍歷方式:
String tmp; long start = System.currentTimeMillis(); // foreach遍歷 for (String s : list) { tmp = s; } System.out.println("foreach spend:" + (System.currentTimeMillis() - start)); // iterator遍歷 start = System.currentTimeMillis(); Iterator<String> it = list.iterator(); while(it.hasNext()) { tmp = it.next(); } System.out.println("iterator spend:" + (System.currentTimeMillis() - start)); // for循環 start = System.currentTimeMillis(); int size = list.size(); for(int i = 0; i < size; i++) { tmp = list.get(i); } System.out.println("for spend:" + (System.currentTimeMillis() - start));
構造一個擁有100萬數據的ArrayList和等價的LinkedList,使用以上代碼測試,測試結果相對耗時如下:
List類型 | ForEach操作 | 叠代器 | for循環 |
ArrayList | 63ms | 47ms | 31ms |
LinkedList | 63ms | 47ms | ∞ |
可以看到,最簡單的ForEach循環並沒有很好的性能表現,綜合性能不如普通的叠代器,而使用for循環通過隨機訪問遍歷列表時,ArrayList表現很好,但是LinkedList的表現卻無法讓人接受。因為LinkedList在進行隨機訪問時,總會進行一次列表的遍歷操作。
對ArrayList這些基於數組的實現來說,隨機訪問速度是很快的,在遍歷這些List對象時,可以優先考慮隨機訪問。但對於基於LinkedList的鏈表實現,隨機訪問非常差,應盡量避免使用。
List接口