下面是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.remove()。例如:
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[]?
最简单的方式可能就是使用Apache Commons Lang库中的ArrayUtils。
int[] array = ArrayUtils.toPrimitive(list.toArray(new Integer[0]));
在JDK中则没有快捷的方法。注意,你不能使用
List.toArray()方法,因为该方法回将
List转换为I
nteger[]。正确的方法如下:
int[] array = new int[list.size()];
for(int i=0; i < list.size(); i++) {
array[i] = list.get(i);
}
4. 如何将int[]转换为List?
最简单的方式可能还是使用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);
}
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的容量,它只会抛出一个IndexOutOfBoundsException异常。有的能可能会问,这样做有什么好处。其中一个原因是,这样可以确保该方法能在线性时间(linear time)内运行完毕。同时,当你只想重用数组而不需要在ArrayList的构造方法中重新分配新的内存空间时,该方法变得十分实用。
其次,Collections.copy()只能将List作为源集合和目标集合,而ArrayList更通用一些,它可以接收Collection参数。