小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言
你是不是发现我们在工作中使用 List 的是实现类一般都是 ArrayList 而不是 LinkedList 为什么会这样呢?我们今天就来一探究竟,ArrayList 和 LinkedList 的区别和联系。
ArrayList 介绍
ArrayList 底层的数据结构是数组,我们在新建一个 ArrayList 的时候初始长度是 0 ,注意这里是 0 ,只有在首次 add 方法的时候才会扩容到 10。
ArrayList 在构建的时候需要一块连续的空间,这是因为底层是数组决定的。
因为 ArrayList 的底层实现是数组,所以它可以随机访问,根据数组下标可以快速的定位到一个元素,JVM 层面在决定某个 List 是否可以随机访问时,是检查有没有实现 RandomAccess 接口,这个接口是一个空接口,就是为了标识那些底层实现是数组的集合。
扩容
以后的每次 add 方法扩容都是 1.5 倍,计算公式是 size >> 1+ size 其实也不是完完全全的 1.5 倍,但是这么说是为了方便。
int newCapacity = oldCapacity + (oldCapacity >> 1);
关于 addAll 方式的扩容,下次扩容的容量和需要的容量对比,选择大的作为扩容后的容量。
Iterator 的 fail-fast 和 fail-safe
fail-fast 简单来说就是不允许一边遍历一边修改,而 fail-safe 允许遍历的时候修改 list, 最新的修改不会被遍历到。
fail-fast 的实现原理又是什么呢?主要的实现是由 Iterator 这个接口的实现类来决定的,所以你会发现我们在使用 ArrayList 的时候如果是迭代器遍历,此时是不能修改的,核心就是两个参数,一个是 ArrayList 的成员变量 modCount 这个值会在每次修改 list 的时候加 1 , 而在 ArrayList 中的 Iterator 实现类中也维护了一个变量 expectedModCount,这个值默认等于 modCount 我们使用迭代器 next 方法的时候都会比较 modCount, expectedModCount 是否相等,如果不相等就会报错,这也就保证了 fail-fast。
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
...
...
...
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
多说一句,对应的 CopyOnWriteArrayList 就是 fail-safe 的,为什么是 safe 就是因为它内部的迭代器没有做相关的检查。为的就是读写分离,读的时候还可以写入。
LinkedList 介绍
LinkedList 的底层实现是链表,我们一般会说,LinkedList 插入快查询慢,其实这是不准确的,应该说是插入头尾快,如果插入的元素在中间,还是要先从头开始找到中间的位置,找到中间的位置本身就是一个耗时动作。
还有一点,LinkedList 因为底层是链表,所有需要的空间可以是不连续的,但是同样的元素下占用的空间要比 ArrayList 大,因为要维护前后索引。
LinkeList 也是 fail-fast 实现。
局部性原理
CPU 缓存会读取元素及其周围的元素一次性加载到缓存中。所以以数组为底层数据结构的 ArrayList 受益于这种特性,而链表却不能受益于这种特性。
内存读取的单位在几百纳秒,而 CPU 的读取在几纳秒,CPU 缓存的读取在十几纳秒到几十纳秒之间。
总结
| ArrayList | LinkedList |
|---|---|
| 基于数组,需要连续内存 | 基于双向链表,无需连续内存 |
| 随机访问快 | 随机访问慢 |
| 尾部插入,删除性能可以,其它部分插入删除都会移动数据,因此性能会低 | 头尾插入删除性能高 |
| 可以利用 CPU 缓存,局部性原理 | 占用内存多 |
经过这么一比较呀,你会发现,ArrayList 真香。