幾種list集合的區別
阿新 • • 發佈:2017-06-03
之間 tor 移動 更多 擁有 要求 測試 出發 最好 SDK提供了有序集合接口java.util.List的幾種實現,其中三種最為人們熟知的是Vector、ArrayList和LinkedList。有關這些List類的性能差別是一個經常被問及的問題。在這篇文章中,我要探討的就是LinkedList和Vector/ArrayList之間的性能差異。
為全面分析這些類之間的性能差異,我們必須知道它們的實現方法。因此,接下來我首先從性能的角度出發,簡要介紹這些類的實現特點。
一、Vector和ArrayList的實現
Vector和ArrayList都帶有一個底層的Object[]數組,這個Object[]數組用來保存元素。通過索引訪問元素時,只需簡單地通過索引訪問內部數組的元素:
public Object get(int index)
{ //首先檢查index是否合法...此處不顯示這部分代碼 return
elementData[index]; }
內部數組可以大於Vector/ArrayList對象擁有元素的數量,兩者的差值作為剩余空間,以便實現快速添加新元素。有了剩余空間,添加元素變得非常簡單,只需把新的元素保存到內部數組中的一個空余的位置,然後為新的空余位置增加索引值:
public boolean add(Object o)
{ ensureCapacity(size + 1); //稍後介紹 elementData[size++] = o; return true;
//List.add(Object) 的返回值 }
把元素插入集合中任意指定的位置(而不是集合的末尾)略微復雜一點:插入點之上的所有數組元素都必須向前移動一個位置,然後才能進行賦值:
public void add(int index, Object element) {
//首先檢查index是否合法...此處不顯示這部分代碼
ensureCapacity(size+1);
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
剩余空間被用光時,如果需要加入更多的元素,Vector/ArrayList對象必須用一個更大的新數組替換其內部Object[]數組,把所有的數組元素復制到新的數組。根據SDK版本的不同,新的數組要比原來的大50%或者100%(下面顯示的代碼把數組擴大100%):
public void ensureCapacity(int minCapacity) {
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = Math.max(oldCapacity * 2, minCapacity);
elementData = new Object[newCapacity];
System.arraycopy(oldData, 0, elementData, 0, size);
}
}
Vector類和ArrayList類的主要不同之處在於同步。除了兩個只用於串行化的方法,沒有一個ArrayList的方法具有同步執行的能力;相反,Vector的大多數方法具有同步能力,或直接或間接。因此,Vector是線程安全的,但ArrayList不是。這使得ArrayList要比Vector快速。對於一些最新的JVM,兩個類在速度上的差異可以忽略不計:嚴格地說,對於這些JVM,這兩個類在速度上的差異小於比較這些類性能的測試所顯示的時間差異。
通過索引訪問和更新元素時,Vector和ArrayList的實現有著卓越的性能,因為不存在除範圍檢查之外的其他開銷。除非內部數組空間耗盡必須進行擴展,否則,向列表的末尾添加元素或者從列表的末尾刪除元素時,都同樣有著優秀的性能。插入元素和刪除元素總是要進行數組復制(當數組先必須進行擴展時,需要兩次復制)。被復制元素的數量和[size-index]成比例,即和插入/刪除點到集合中最後索引位置之間的距離成比例。對於插入操作,把元素插入到集合最前面(索引0)時性能最差,插入到集合最後面時(最後一個現有元素之後)時性能最好。隨著集合規模的增大,數組復制的開銷也迅速增加,因為每次插入操作必須復制的元素數量增加了。
二、LinkedList的實現
LinkedList通過一個雙向鏈接的節點列表實現。要通過索引訪問元素,你必須查找所有節點,直至找到目標節點:
public Object get(intindex) {
//首先檢查index是否合法...此處不顯示這部分代碼
Entry e = header; //開始節點
//向前或者向後查找,具體由哪一個方向距離較
//近決定
if (index < size/2) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
把元素插入列表很簡單:找到指定索引的節點,然後緊靠該節點之前插入一個新節點:
public void add(int index, Object element) {
//首先檢查index是否合法...此處不顯示這部分代碼
Entry e = header; //starting node
//向前或者向後查找,具體由哪一個方向距離較
//近決定
if (index < size/2) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
Entry newEntry = new Entry(element, e, e.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
}
線程安全的LinkedList和其他集合
如果要從Java SDK得到一個線程安全的LinkedList,你可以利用一個同步封裝器從Collections.synchronizedList(List)得到一個。然而,使用同步封裝器相當於加入了一個間接層,它會帶來昂貴的性能代價。當封裝器把調用傳遞給被封裝的方法時,每一個方法都需要增加一次額外的方法調用,經過同步封裝器封裝的方法會比未經封裝的方法慢二到三倍。對於象搜索之類的復雜操作,這種間接調用所帶來的開銷不是很突出;但對於比較簡單的方法,比如訪問功能或者更新功能,這種開銷可能對性能造成嚴重的影響。
這意味著,和Vector相比,經過同步封裝的LinkedList在性能上處於顯著的劣勢,因為Vector不需要為了線程安全而進行任何額外的間接調用。如果你想要有一個線程安全的LinkedList,你可以復制LinkedList類並讓幾個必要的方法同步,這樣你可以得到一個速度更快的實現。對於所有其它集合類,這一點都同樣有效:只有List和Map具有高效的線程安全實現(分別是Vector和Hashtable類)。有趣的是,這兩個高效的線程安全類的存在只是為了向後兼容,而不是出於性能上的考慮。
對於通過索引訪問和更新元素,LinkedList實現的性能開銷略大一點,因為訪問任意一個索引都要求跨越多個節點。插入元素時除了有跨越多個節點的性能開銷之外,還要有另外一個開銷,即創建節點對象的開銷。在優勢方面,LinkedList實現的插入和刪除操作沒有其他開銷,因此,插入-刪除開銷幾乎完全依賴於插入-刪除點離集合末尾的遠近。
為全面分析這些類之間的性能差異,我們必須知道它們的實現方法。因此,接下來我首先從性能的角度出發,簡要介紹這些類的實現特點。
一、Vector和ArrayList的實現
Vector和ArrayList都帶有一個底層的Object[]數組,這個Object[]數組用來保存元素。通過索引訪問元素時,只需簡單地通過索引訪問內部數組的元素:
public Object get(int index)
{ //首先檢查index是否合法...此處不顯示這部分代碼 return
elementData[index]; }
內部數組可以大於Vector/ArrayList對象擁有元素的數量,兩者的差值作為剩余空間,以便實現快速添加新元素。有了剩余空間,添加元素變得非常簡單,只需把新的元素保存到內部數組中的一個空余的位置,然後為新的空余位置增加索引值:
public boolean add(Object o)
{ ensureCapacity(size + 1); //稍後介紹 elementData[size++] = o; return true;
//List.add(Object) 的返回值 }
把元素插入集合中任意指定的位置(而不是集合的末尾)略微復雜一點:插入點之上的所有數組元素都必須向前移動一個位置,然後才能進行賦值:
public void add(int index, Object element) {
//首先檢查index是否合法...此處不顯示這部分代碼
ensureCapacity(size+1);
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
剩余空間被用光時,如果需要加入更多的元素,Vector/ArrayList對象必須用一個更大的新數組替換其內部Object[]數組,把所有的數組元素復制到新的數組。根據SDK版本的不同,新的數組要比原來的大50%或者100%(下面顯示的代碼把數組擴大100%):
public void ensureCapacity(int minCapacity) {
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = Math.max(oldCapacity * 2, minCapacity);
elementData = new Object[newCapacity];
System.arraycopy(oldData, 0, elementData, 0, size);
}
}
Vector類和ArrayList類的主要不同之處在於同步。除了兩個只用於串行化的方法,沒有一個ArrayList的方法具有同步執行的能力;相反,Vector的大多數方法具有同步能力,或直接或間接。因此,Vector是線程安全的,但ArrayList不是。這使得ArrayList要比Vector快速。對於一些最新的JVM,兩個類在速度上的差異可以忽略不計:嚴格地說,對於這些JVM,這兩個類在速度上的差異小於比較這些類性能的測試所顯示的時間差異。
通過索引訪問和更新元素時,Vector和ArrayList的實現有著卓越的性能,因為不存在除範圍檢查之外的其他開銷。除非內部數組空間耗盡必須進行擴展,否則,向列表的末尾添加元素或者從列表的末尾刪除元素時,都同樣有著優秀的性能。插入元素和刪除元素總是要進行數組復制(當數組先必須進行擴展時,需要兩次復制)。被復制元素的數量和[size-index]成比例,即和插入/刪除點到集合中最後索引位置之間的距離成比例。對於插入操作,把元素插入到集合最前面(索引0)時性能最差,插入到集合最後面時(最後一個現有元素之後)時性能最好。隨著集合規模的增大,數組復制的開銷也迅速增加,因為每次插入操作必須復制的元素數量增加了。
二、LinkedList的實現
LinkedList通過一個雙向鏈接的節點列表實現。要通過索引訪問元素,你必須查找所有節點,直至找到目標節點:
public Object get(intindex) {
//首先檢查index是否合法...此處不顯示這部分代碼
Entry e = header; //開始節點
//向前或者向後查找,具體由哪一個方向距離較
//近決定
if (index < size/2) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
把元素插入列表很簡單:找到指定索引的節點,然後緊靠該節點之前插入一個新節點:
public void add(int index, Object element) {
//首先檢查index是否合法...此處不顯示這部分代碼
Entry e = header; //starting node
//向前或者向後查找,具體由哪一個方向距離較
//近決定
if (index < size/2) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
Entry newEntry = new Entry(element, e, e.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
}
線程安全的LinkedList和其他集合
如果要從Java SDK得到一個線程安全的LinkedList,你可以利用一個同步封裝器從Collections.synchronizedList(List)得到一個。然而,使用同步封裝器相當於加入了一個間接層,它會帶來昂貴的性能代價。當封裝器把調用傳遞給被封裝的方法時,每一個方法都需要增加一次額外的方法調用,經過同步封裝器封裝的方法會比未經封裝的方法慢二到三倍。對於象搜索之類的復雜操作,這種間接調用所帶來的開銷不是很突出;但對於比較簡單的方法,比如訪問功能或者更新功能,這種開銷可能對性能造成嚴重的影響。
這意味著,和Vector相比,經過同步封裝的LinkedList在性能上處於顯著的劣勢,因為Vector不需要為了線程安全而進行任何額外的間接調用。如果你想要有一個線程安全的LinkedList,你可以復制LinkedList類並讓幾個必要的方法同步,這樣你可以得到一個速度更快的實現。對於所有其它集合類,這一點都同樣有效:只有List和Map具有高效的線程安全實現(分別是Vector和Hashtable類)。有趣的是,這兩個高效的線程安全類的存在只是為了向後兼容,而不是出於性能上的考慮。
對於通過索引訪問和更新元素,LinkedList實現的性能開銷略大一點,因為訪問任意一個索引都要求跨越多個節點。插入元素時除了有跨越多個節點的性能開銷之外,還要有另外一個開銷,即創建節點對象的開銷。在優勢方面,LinkedList實現的插入和刪除操作沒有其他開銷,因此,插入-刪除開銷幾乎完全依賴於插入-刪除點離集合末尾的遠近。
幾種list集合的區別