有關Java集合類的10大問題
下面是StackOverflow上最受歡迎的有關Java集合類的問題。在看這些問題之前,你最好先看看《集合類的介面和類層次結構圖及示例程式》。
1. 什麼時候LinkedList比ArrayList更適用?
ArrayList本質上是一個數組,可以直接通過索引(index)訪問其中的元素。一旦陣列被填滿,就需要重新分配一個更大的陣列,並將原陣列的中的所有元素按順序複製到新陣列中,這需要耗費O(n)的時間。而且,在ArrayList中新增或移除一個元素都需要移動陣列現有的元素。這可能是使用ArrayList的最大劣勢。
LinkedList是一個雙向連結串列。因此,如果需要訪問連結串列中間的元素,需要從連結串列的頭部開始查詢。但是,在LinkedList新增和移除元素的速度比較快,因為者只需要對連結串列進行區域性修改。
在最壞情況下,ArrayList和LinkedList個方法的時間複雜度總結如下:
ArrayList | LinkedList | |
get(index) | O(1) | O(n) |
add(E) | O(n) | O(1) |
add(E, index) | O(n) | O(n) |
remove(index) | O(n) | O(n) |
Iterator.remove() | O(n) | O(1) |
Iterator.add(E) | O(n) | O(1) |
除了執行時間,對於大的list,還需要考慮記憶體的使用。在LinkedList中,每個指點中至少需要兩個額外的指標分別指向前一個節點和下一個節點;而在ArrayList中,只需要一個包含元素的陣列。
2. 對Collection很行迭代時,移除元素的方法比較
Iterator<Integer> itr = list.iterator();
while(itr.hasNext()) {
// do something
itr.remove();
}
經常被使用的一段錯誤程式碼是:for(Integer i: list) {
list.remove(i);
}
在執行上面的程式碼是,會出現ConcurrentModificationException異常。這是因為for迴圈中已經生成一個迭代器來遍歷整個list,同時,list又被Iterator.remove()方法修改。在Java中,”如果一個執行緒正在對集合進行迭代,就不允許另一個執行緒對集合進行修改“。
3. 如何將List轉換為int[]?
int[] array = ArrayUtils.toPrimitive(list.toArray(new Integer[0]));
在JDK中則沒有快捷的方法。注意,你不能使用List.toArray()方法,因為該方法回將List轉換為Integer[]。正確的方法如下:
int[] array = new int[list.size()];
for(int i=0; i < list.size(); i++) {
array[i] = list.get(i);
}
4. 如何將int[]轉換為List?
List list = Arrays.asList(ArrayUtils.toObject(array));
同樣,在JDK中也沒有快捷的方法。int[] array = {1,2,3,4,5};
List<Integer> list = new ArrayList<Integer>();
for(int i: array) {
list.add(i);
}
5. 篩選Collection中元素的最好方式是什麼?
同樣,你可以使用第三方的包來實現該功能,如Guava或Apache Commons Lang。這兩個包都提供了一個filter()方法(在Guava的Collections2中和Apache的CollectionUtils中)。filter()方法返回符合指定Predicate的元素。
在JDK中,實現這一功能比較難。好訊息是,在Java中會新增Predicate。但現在,你必須使用Iterator來遍歷整個集合。
Iterator<Integer> itr = list.iterator();
while(itr.hasNext()) {
int i = itr.next();
if (i > 5) { // filter all ints bigger than 5
itr.remove();
}
}
當然,你可以模仿Guava和Apache引入一個新的Predicate介面。這也是大多數高階開發人員採用的方式。
public interface Predicate<T> {
boolean test(T o);
}
public static <T> void filter(Collection<T> collection, Predicate<T> predicate) {
if ((collection != null) && (predicate != null)) {
Iterator<T> itr = collection.iterator();
while(itr.hasNext()) {
T obj = itr.next();
if (!predicate.test(obj)) {
itr.remove();
}
}
}
}
這樣,我們就可以使用下面的程式碼來篩選集合中的元素:
filter(list, new Predicate<Integer>() {
public boolean test(Integer i) {
return i <= 5;
}
});
6. 將List轉換為Set的最簡單的方式
根據你對equal定義的不同,可以有兩種實現該功能的方式。第一段程式碼將list放入一個HashSet中,通過hashCode()方法可以辨別重複的元素。在大多數情況下,這種方式是有效的。然而,你需要指明比較的方式。當你能夠定義自己的比較器時,最好使用第二段程式碼。
Set<Integer> set = new HashSet<Integer>(list);
Set<Integer> set = new TreeSet<Integer>(aComparator);
set.addAll(list);
7. 我該如何從ArrayList中移除重複的元素?
這個問題和上一個問題十分相似。
如果你不在乎ArrayList中元素的順序,一種比較聰明的方式是先將list放入set中來去除重複的元素,然後再將set中的元素移回到list。程式碼如下:
ArrayList** list = ... // initial a list with duplicate elements
Set<Integer> set = new HashSet<Integer>(list);
list.clear();
list.addAll(set);
如果你比較在乎元素的順序,那麼就沒有快捷的方式了,至少需要兩次迴圈。
8. 對Collection進行排序
在Java中有若干種方式來維護一個可排序的集合。這些方式都通過定義一個比較器來提供一個自然排序的集合。為了實現自然排序,你需要讓集合中的元素實現Comparable介面。
1. Collections.sort()方法能夠對List進行排序。正如javadoc說明的那樣,該方法的排序結果是穩定的,而且保證有nlog(n)的效能。
2. PriorityQueue提供了一種有序的佇列。PriorityQueue和Collections.sort()的區別在於,PriorityQueue實時維護一個有序佇列,但你只能從佇列中獲得隊頭元素。你不能像用PriorityQueue.get(4)這樣的方法來隨機訪問其中元素。
3. 如果集合中沒有重複的元素,我們還可以使用TreeSet。與PriorityQueue類似,TreeSet也是實時維護一個有序集合。你可以從TreeSet中獲取第一個和最後一個元素,但不能隨機訪問其中的元素。
簡而言之,Collections.sort()提供了一次性的有序list。PriorityQueue和TreeSet則以損失通過索引訪問元素的方式為代價,實現對有序集合的實時維護。
9. Collections.emptyList()方法 vs 建立新的例項(new instance)
這一問題也適用於emptyMap()和emptySet()。
這兩種方法都返回一個空的list,但Collections.emptyList()返回一個不可變的list,這意味著你無法將新的元素新增進該”空的”list中。在這種情況下,每一次呼叫Collections.emptyList()實際上都沒有建立一個新的空的list例項。事實上,該方法僅僅是重用了已有的空list例項。如果你熟悉單例設計模式,你就應該能明白我的意思。如果你經常呼叫該方法,你會發現它的效能很高。
10. Collections.copy()方法
有兩種方式可以將一個源list(source list)複製到目標list(destination list)中。一種方式是使用ArrayList的構造方法。
ArrayList<Integer> dstList = new ArrayList<Integer>(srcList);
另一種方式是使用Collections.copy()方法(如下所示)。請注意第一行,我們新分配了一個list,它的長度至少要和源list相同。這是因為在Collections的javadoc中指明,目標list至少要和源list一樣長。
ArrayList<Integer> dstList = new ArrayList<Integer>(srcList.size());
Collections.copy(dstList, srcList);
這兩種方式都是通過淺拷貝(shallow copy)來實現的。那麼,它們的區別在哪兒呢?
首先,即使dstList沒有足夠的空間來容納srcList中所有的元素,Collections.copy()方法也不會重新分配dstList的容量,它只會丟擲一個異常。有的能可能會問,這樣做有什麼好處。其中一個原因是,這樣可以確保該方法能線上性時間(linear time)內執行完畢。同時,當你只想重用陣列而不需要在ArrayList的構造方法中重新分配新的記憶體空間時,該方法變得十分實用。
其次,Collections.copy()只能將List作為源集合和目標集合,而ArrayList更通用一些,它可以接收Collection引數。