对比Vector、ArrayList、LinkedList

对比Vector、ArrayList、LinkedList有何区别?

我们在日常的工作中,能够高效地管理和操作数据是非常重要的。由于每个编程语言支持的数据结构不尽相同,比如我最早学习的 C 语言,需要自己实现很多基础数据结构,管理和 操作会比较麻烦。相比之下, Java 则要方便的多,针对通用场景的需求, Java 提供了强大的集合框架,大大提高了开发者的生产力。

基础解释

这三者都是实现集合框架中的 List ,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置进行定位、添加或者删除的操作,都提供迭代器以遍历其内容等。但因 为具体的设计区别,在行为、性能、线程安全等方面,表现又有很大不同。

ArrayList是应用更加广泛的 动态数组 实现,它本身不是线程安全的,所以性能要好很多。与Vector近似,ArrayList也是可以根据需要调整容量,不过两者的调整逻辑有所区 别, Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50% 。

LinkedList顾名思义是Java提供的 双向链表 ,所以它不需要像上面两种那样调整容量,它也不是线程安全的。

分析

  • Vector和ArrayList作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。

  • 而LinkedList进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。

所以,在应用开发中,如果事先可以估计到,应用操作是偏向于插入、删除,还是随机访问较多,就可以针对性的进行选择。这也是面试最常见的一个考察角度,给定一个场景,选 择适合的数据结构,所以对于这种典型选择一定要掌握清楚。

知识扩展

我们先一起来理解集合框架的整体设计,为了有个直观的印象,我画了一个简要的类图。注意,为了避免混淆,我这里没有把 java.util.concurrent 下面的线程安全容器添加进来; 也没有列出 Map 容器,虽然通常概念上我们也会把 Map 作为集合框架的一部分,但是它本身并不是真正的集合( Collection )。

我们可以看到 Java 的集合框架, Collection 接口是所有集合的根,然后扩展开提供了三大类集合,分别是:

  • List,也就是我们前面介绍最多的有序集合,它提供了方便的访问、插入、删除等操作。

  • Set,Set是不允许重复元素的,这是和List最明显的区别,也就是不存在两个对象equals返回true。我们在日常开发中有很多需要保证元素唯一性的场合。

  • Queue/Deque,则是Java提供的标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO, First-in-First-Out)或者后入先出(LIFO,Last-In-FirstOut )等特定行为。这里不包括 BlockingQueue ,因为通常是并发编程场合,所以被放置在并发包里。

TreeSet代码里实际默认是利用TreeMap实现的,Java类库创建了一个Dummy对象“PRESENT”作为value,然后所有插入的元素其实是以 键的形式放入了 TreeMap 里面;同理, HashSet 其实也是以 HashMap 为基础实现的,原来他们只是 Map 类的马甲!

就像前面提到过的,我们需要对各种具体集合实现,至少了解基本特征和典型使用场景,以 Set 的几个实现为例:

  • TreeSet支持自然顺序访问,但是添加、删除、包含等操作要相对低效(log(n)时间)。

  • HashSet则是利用哈希算法,理想情况下,如果哈希散列正常,可以提供常数时间的添加、删除、包含等操作,但是它不保证有序。

  • LinkedHashSet,内部构建了一个记录插入顺序的双向链表,因此提供了按照插入顺序遍历的能力,与此同时,也保证了常数时间的添加、删除、包含等操作,这些操作性能略 低于 HashSet ,因为需要维护链表的开销。

  • 在遍历元素时,HashSet性能受自身容量影响,所以初始化时,除非有必要,不然不要将其背后的HashMap容量设置过大。而对于LinkedHashSet,由于其内部链表提供的方 便,遍历性能只和元素多少有关系。

我今天介绍的这些集合类,都不是线程安全的,对于 java.util.concurrent 里面的线程安全容器,我在专栏后面会去介绍。但是,并不代表这些集合完全不能支持并发编程的场景, 在 Collections 工具类中,提供了一系列的 synchronized 方法,比如

1
static <T> Lis<T> synchronizedLis(List<T> list)

我们完全可以利用类似方法来实现基本的线程安全集合:

1
List list = Collections.synchronizedList(new ArrayList());

它的实现,基本就是将每个基本方法,比如 get 、 set 、 add 之类,都通过 synchronizd 添加基本的同步支持,非常简单粗暴,但也非常实用。注意这些方法创建的线程安全集合,都 符合迭代时 fail-fast 行为,当发生意外的并发修改时,尽早抛出 ConcurrentModifcationException 异常,以避免不可预计的行为。

另外一个经常会被考察到的问题,就是理解 Java 提供的默认排序算法,具体是什么排序方式以及设计思路等。

这个问题本身就是有点陷阱的意味,因为需要区分是 Arrays.sort() 还是 Collections.sort() (底层是调用 Arrays.sort() );什么数据类型;多大的数据集(太小的数据集,复杂排 序是没必要的, Java 会直接进行二分插入排序)等。

  • 对于原始数据类型,目前使用的是所谓双轴快速排序(Dual-Pivot QuickSort),是一种改进的快速排序算法,早期版本是相对传统的快速排序,你可以阅读 源码 。

  • 而对于对象数据类型,目前则是使用TimSort,思想上也是一种归并和二分插入排序(binarySort)结合的优化排序算法。TimSort并不是Java的独创,简单说它的思路是查找数据集中已经排好序的分区(这里叫 run ),然后合并这些分区来达到排序的目的。

另外, Java 8 引入了并行排序算法(直接使用 parallelSort 方法),这是为了充分利用现代多核处理器的计算能力,底层实现基于 fork-join 框架(专栏后面会对 fork-join 进行相对详 细的介绍),当处理的数据集比较小的时候,差距不明显,甚至还表现差一点;但是,当数据集增长到数万或百万以上时,提高就非常大了,具体还是取决于处理器和系统环境。

排序算法仍然在不断改进,最近双轴快速排序实现的作者提交了一个更进一步的改进,历时多年的研究,目前正在审核和验证阶段。根据作者的性能测试对比,相比于基于归并排序 的实现,新改进可以提高随机数据排序速度提高 10% ~ 20% ,甚至在其他特征的数据集上也有几倍的提高,有兴趣的话你可以参考具体代码和介绍

阅读Java源代码,你会发现,这些API的设计和实现比较独特,它们并不是实现在抽象类里面,而是以 默认方法 的形式实现在Collection这样的接口里!这是Java 8在语言层面的新 特性,允许接口实现默认方法,理论上来说,我们原来实现在类似 Collections 这种工具类中的方法,大多可以转换到相应的接口上。针对这一点,我在面向对象主题,会专门梳 理 Java 语言面向对象基本机制的演进。

在 Java 9 中, Java 标准类库提供了一系列的静态工厂方法,比如, List.of() 、 Set.of() ,大大简化了构建小的容器实例的代码量。根据业界实践经验,我们发现相当一部分集合实例 都是容量非常有限的,而且在生命周期中并不会进行修改。但是,在原有的 Java 类库中,我们可能不得不写成:

1
2
3
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");

而利用新的容器静态工厂方法,一句代码就够了,并且保证了不可变性。

1
List<String> simpleList = List.of("a","b");

更进一步,通过各种of静态工厂方法创建的实例,还应用了一些我们所谓的最佳实践,比如,它是不可变的,符合我们对线程安全的需求;它因为不需要考虑扩容,所以空间上更加紧凑等。

如果我们去看 of 方法的源码,你还会发现一个特别有意思的地方:我们知道 Java 已经支持所谓的可变参数( varargs ),但是官方类库还是提供了一系列特定参数长度的方法,看起 来似乎非常不优雅,为什么呢?这其实是为了最优的性能, JVM 在处理变长参数的时候会有明显的额外开销,如果你需要实现性能敏感的 API ,也可以进行参考。

补充

Vector、ArrayList、LinkedList均为线型的数据结构, 但是从实现方式与应用场景中又存在差别。

底层实现方式

ArrayList内部用数组来实现;LinkedList内部采用双向链表实现;Vector内部用数组实现。

读写机制

ArrayList在执行插入元素是超过当前数组预定义的最大值时, 数组需要扩容,扩容过程需要调用底层System.arraycopy()方法进行大量的数组复制操作;

在删除元素时并不会减少数组的容量 (如果需要缩小数组容量,可以调用trimToSize()方法);在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找。

LinkedList在插入元素时, 须创建一个新的Entry对象,并更新相应元素的前后元素的引用;
在查找元素时,需遍历链表;在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元 素删除即可。

Vector与ArrayList仅在插入元素时容量扩充机制不一致。
对于Vector,默认创建一个大小为10的Object数组,并将capacityIncrement设置为0;

当插入元素数组大小不够时,如 果capacityIncrement大于0,则将Object数组的大小扩大为现有size+capacityIncrement;
如果capacityIncrement<=0,则将Object数组的大小扩大为现有大小的2倍。

读写效率

ArrayList对元素的增加和删除都会引起数组的内存分配空间动态发生变化。 因此,对其进行插入和删除速度较慢,但检索速度很快。 LinkedList由于基于链表方式存放数据, 增加和删除元素的速度较快,但是检索速度较慢。

线程安全性

ArrayList、LinkedList为非线程安全;Vector是基于synchronized实现的线程安全的ArrayList。

需要注意的是:单线程应尽量使用ArrayList, Vector因为同步会有性能损耗;即使在多线程环境下, 我们可以利用Collections这个类中为我们提供的synchronizedList(List list)方法返回一个 线程安全的同步列表对象。

Author: Vallzey
Link: https://vallzey.github.io/2018/05/22/对比Vector、ArrayList、LinkedList/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.