以下是一些在Stackoverflow上經常被問起的與Java集合相關的問題。在你查閱這些問題之前,最好先去看看【Simple Java】Java集合框架的接口和類層次關系結構圖。
ArrayList本質上是一個數組,它的元素可以直接通過索引訪問。但是,當數組滿的時候,需要申請新的更大的數組空間,並將所有元素復制到新數組中,這將花費O(n)的時間。另外,插入和刪除元素需要移動數組中的其它元素,這也許是ArrayList最大的劣勢。
LinkedList是一個雙向鏈表,因此,當訪問鏈表中間的元素的時候,它需要從鏈表的頭結點處開始搜索。但好的一方面是,對於插入和刪除操作,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)
除了運行時間外,內存空間的使用也應該被考慮,特別是對於大的列表。在LinkedList中,每個節點需要兩個額外的指針指向前節點和後節點,然而ArrayList只需要一個存放元素的數組即可。
其它List之間的比較,可查閱:【Simple Java】ArrayList vs LinkedList vs Vector。
在迭代集合的時候,唯一正確的方法修改集合的方法是通過Iterator.remove()方法,如下示例:
Iterator<Integer> itr = list.iterator(); while(itr.hasNext()) { // do something itr.remove(); }
另外,舉一個常見的錯誤代碼:
for(Integer i: list) { list.remove(i); }
運行以上錯誤代碼,你將會得到ConcurrentModificationException異常,這是因為以上代碼產生了一個迭代器(由for語句產生)去遍歷列表,但是同時,列表被修改了(通過Iterator.remove())。在Java中,一個線程在迭代遍歷集合的過程中,是不允許另外一個線程去修改集合的。
最簡單的方法是使用Apache Commons Lang庫下的ArrayUtils工具類,如下:
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); }
最簡單的方法仍然是使用Apache Commons Lang的ArrayUtils工具類,如下:
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); }
同樣,你可以使用第三方庫,如google的Guava庫或Apache Commons Lang去實現這個功能,兩者都提供了filter()方法(Guava的Collections2或Apache的CollectionUtils類)。filter()方法會返回匹配的元素。
在JDK中,這將會變得困難,好消息是,在java 8中,增加了Predicate接口,可以實現該功能。但是現在,我們只能使用迭代器去遍歷整個集合:
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; } });
有兩種方法來實現該功能,取決於你如何定義“相等”。第一種方法將list存入HashSet,元素的重復主要由hashCode()來區分,大多數情況下,這是可行的。但是,如果你需要自己定義相等的比較方式,最好使用第二種方法,定義自己的比較器。
Set<Integer> set = new HashSet<Integer>(list);
Set<Integer> set = new TreeSet<Integer>(aComparator); set.addAll(list);
這個問題和上一個很像,如果你不關心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);
如果你關心元素的順序的話,可以使用標准JDK中的LinkedHashSet來實現該功能。
Java中有幾種方式來維持集合中元素的順序,它們提供了默認的排序順序或者通過指定比較器來排序。不過即使是默認的排序,集合中的任何元素也需要實現Comparable接口。
簡單的說,Collections.sort()提供了對List的一次性排序,PriorityQueue和TreeSet能一直維持集合中元素的順序,但是不能隨即訪問元素。
該問題也適用於emptyMap()和emptySet()。
這兩種方式都返回了一個空集合,但是Collections.emptyList()返回的是一個不可變集合,意味著你不能往這個空集合新增元素。事實上,每次調用Collections.emptyList()並不會創建一個空集合,而是復用已經存在的空集合實例。如果你熟悉單例模式的話,你應該知道我所說的,如果你頻繁調用的話,這將會提供更好的性能。
有兩種方法講一個List集合拷貝到另外一個List集合,其中一種是使用ArrayList的構造方法,如下:
ArrayList<Integer> dstList = new ArrayList<Integer>(srcList);
另一種是使用Collections.copy()方法(如下),注意第一行,我們分配了一個和源List集合長度相等的初始容量。
ArrayList<Integer> dstList = new ArrayList<Integer>(srcList.size()); Collections.copy(dstList, srcList);
這兩種方法都使用淺拷貝,那麼這兩種方法的區別是什麼呢?
譯文鏈接:http://www.programcreek.com/2013/09/top-10-questions-for-java-collections/