前言
对不少人来说,List恐怕是最常用的集合了。java中,List作为接口,其下有两个常见的实现类:ArrayList和LinkedList。除此之外,还提供了AbstractList抽象类,用来帮助自定义List的实现类。本文先从AbstractList入手,再对比ArrayList和LinkedList,从而将这两个常用的集合做一个大体的介绍。
在具体的介绍之前,笔者希望先讨论一个概念:fail-fast。
fail-fast是java集合框架中,一个可以通过快速抛出ConcurrentModificationException、来通知某不支持并发的集合被多个线程、进行了修改的机制。既然命名为”fast”,往往意味着作为一个快速通道,并不涵盖所有的情况。事实也正是如此。在许多集合的实现中,只有发生了结构化修改(集合的结构产生了变动,如增加、减少了结点)时,才会触发fail-fast机制。因此,fail-fast的相关资料通常会有这样的表述:
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
导航
AbstractList
AbstractList作为许多List实现类的祖先类,它的fail-fast实现规则也被一些子类所继承(如ArrayList与LinkedList)。我们先来看一下AbstractList中迭代器触发fail-fast的机制:
1 | private class Itr implements Iterator<E> { |
可以看到,触发的关键在于modCount与Itr中的字段expectedModCount。由于AbstractList默认实现是不可修改的,因此讨论modCount什么时候被修改、fail-fast什么时候被触发,还得放到它的子类当中来具体分析。
除了fail-fast,AbstractList还涉及到了另一个概念:视图。
1 | // 以原list为底,返回sublist |
subList方法返回原list的一部分,称之为视图。需要注意的是,视图以原list为底操作,任何对视图的修改都会反应到原list上,原list的任何变动也会对相关视图造成影响。
1 | List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); |
ArrayListAndLinkedList
说到ArrayList和LinkedList的区别,各种资料非常多。我们暂时先不管各种说法,单纯看看它们各自在java中的类定义:
1 | // ArrayList实现随机访问接口RandomAccess |
所以,两者最明显的区别就显而易见了:ArrayList支持随机访问(通过下标随机访问元素),而LinkedList不行,只能通过顺序访问。
那么问题来了,实际使用中,LinkedList显然是可以通过get(int index)获取元素的,那为什么说LinkedList只支持顺序访问呢?
为了回答这个问题,先来看看两者get(int index)方法的实现:
1 | // ArrayList |
1 |
|
原来LinkedList的get(int index)是通过遍历并计算下标才实现的,get方法的随机访问只是一个假象,ArrayList才是真正的随机访问。
接着,我们可以来看看ArrayList和LinkedList为什么会有这个区别了。总的来说,因为两者实现的数据结构不同:ArrayList采用数组实现,而LinkedList采取双向链表实现。数组实现的优势在于支持通过下标进行随机访问,通常情况下的顺序遍历速度也快于链表,但在发生结构化变动、尤其是从中间删除元素之时,整个数组的变动调整将会很大。而链表就没有这个风险了,只需要改动几个结点的引用罢了。
1 | // ArrayList |
1 | // LinkedList |
有点读者可能注意到了,上面的代码出现了一个前文提到过的字段:modcount。实际上,在ArrayList与LinkedList之中,只有发生了结构化的修改(如add、remove、sort方法),modcount才会更改。
现在,我们来仔细看看,Itr中的remove方法:
1 | public void remove() { |
通常,我们使用List而不是Array的主要原因,在于List几乎不需要在意容量大小。某种程度上,List可以看做自动扩容的数组,尤其是本身就以数组实现的ArrayList。在ArrayList中,自动扩容的方案为:当不满足所需的最小容量时,将会先扩展至原容量的1.5倍。仍然不足,才会扩展至需求的最小容量。也因此,ArrayList的空间占用,往往是大于LinkedList的。
1 | private void grow(int minCapacity) { |