本文从ArrayList和LinkedList的不同点入手,到增加/删除元素API的效率比较,顺带扩充知识面:简述随机访问接口RandomAccess、vector集合,最后以ArrayList何时扩容、如何扩容(源码分析)收尾。
热身一下:List、Set、Map的区别?
- List存储一组不唯一、可重复的对象,即可以有多个元素引用相同的对象。
- Set是一个不允许重复的集合,即不会有多个元素引用相同的对象。
- Map键值对存储,每个value与一个独一无二的key关联,两个Key可以引用相同的对象,单Key不能重复。如果使用自定义对象作为Key的时候,经常需要重写
hashCode()
和equals()
。
ArrayList和LinkedList的不同点
两者都是非线程安全的,但是底层数据结构不同:
ArrayList
底层使用Object数组
;LinkedList
底层JDK1.6及之前采用循环双向链表
,之后采用的都是双向链表
。正因为二者底层数据结构的不同,内存空间浪费的主要体现也不同:
ArrayList
主要是在列表结尾经常会预留一定的容量空间;而
LinkedList
的空间花费主要体现在其每个数据元素,都需要消耗一定的额外空间(因为要存储元素的前驱和后继)。数据结构不同,决定了他们各种操作之间的效率不同,拿
add和remove
来说:ArrayList
的Add(E e)
方法会默认将e追加到列表末尾,时间O(1),但是如果指定插入位置index的操作add(int index, E element)
,时间就是O(n),因为要将index之后的元素都向后移动,remove
操作也是O(n),因为要将被移除元素之后的所有元素向前移动;相较
LinkedList
的插入操作add(E e)
和remove
操作虽然整体也是O(n),但原因是需要线性查找元素或指定位置所造成的。*虽然 ArrayList 和 LinkedList 的 add/remove 方法都是 O(n),但是在很多场景下 ArrayList 会略快一点。因为影响其时间的大量元素移位操作是调用 System.arraycopy() 进行内存连续空间拷贝完成的。这一点相较于
LinkedList
的大量非连续性地址访问,最终还是略胜一筹的。 *关于arraycopy()
方法文末会详细分析。之所以上述
LinkedList
的操作必须要有线性查找,原因就是其不支持高效的随机元素访问;而ArrayList
则天然的支持这一点。
RandomAccess接口
1 | public interface RandomAccess {} |
空接口,只是起到一个标识的作用罢了!标识实现此接口的类具有随机访问的特性。
因此ArrayList
实现了此接口,而LinkedList
没有实现。
在Collections
的binarySearch
方法中就有对这个空接口的应用:
1 | public static <T> |
只要有一些 Java 语法基础就很容易看懂这个接口的应用。
从上述代码的 if else 分支不难总结出list
的遍历方式如何选择:
- 如果是实现了
RandomAccess
接口的 list,因为其支持高效的随机元素访问,所以优先选择普通的 for 循环进行遍历,其次是 foreach。 - 如果是没有实现
RandomAccess
接口的 list,优先选择iterator
遍历(foreach遍历的底层也是采用iterator实现的),尤其是数据量较大的情况下,决不能使用普通for循环。
Vector
Vector
类已经被ArrayList
取代了,虽然Vector
类的方法都是线程安全的,但是其在同步操作上耗费的时间消耗过于巨大。
ArrayList扩容
既然ArrayList
底层选择Object
数组实现,那么就不得不面对数组的一大麻烦:扩容!
构造方法
既然说其”扩”容,那就不得不提何时扩容?先看一下其几个常用构造方法:
1 | /** |
可以指定ArrayList
的初始数组大小、可以指定包含collection的元素(利用该集合的迭代器按顺序返回其中的元素),如果选择无参构造方法创建ArrayList
时,虽然类中定义了默认大小10,实际上初始化赋值的是一个空数组,只有当真正向集合中添加元素时,才会真正的分配容量。即向集合添加第一个元素时,数组扩容至默认大小10。
何时扩容?
以无参构造函数创建的ArrayList
为例进行分析,第一次扩容发生在添加第一个元素时,那么我们就先看看add(E e)
中是如何进行的:
1 | /** |
添加元素之前先调用ensureCapacityInternal(size+1)
:
1 | //得到最小扩容量 |
从add(E e)
进来之后,minCapacity
为1,在max()
之后,变成了默认容量10。
之后调用ensureExplicitCapacity(minCapacity)
方法:
1 | //判断是否需要扩容 |
可以看到这是调用扩容方法grow()
之前的最后一步,仔细分析一下:
- 此时
minCapacity
为10,而elementData.length
为0(依然是最初的那个空数组),所以10-0>0
成立,执行grow()
开始扩容。 - 为什么是
minCapacity - elementData.length > 0
?从add(E e)
方法传参size+1
,到max(默认容量,size+1)
传递到这一步,拿到了添加元素之后数组的最小容量minCapacity
,减去当前数组的容量elementData.length
,结果>0
即当前数组大小不足以添加新元素,进行扩容。
如何扩容?
扩容自然是从grow()
方法开始:
1 | /** |
总结一下扩容方法:
先扩容至旧容量
oldCapacity
的 1.5 倍。如果不够就扩容至最小容量
minCapacity
。还要检查扩容后的新容量
newCapacity
是否超过最大数组容量MAX_VALUE-8
。如果超过最大容量,则执行hugeCapacity
方法:1
2
3
4
5
6
7
8
9
10
11private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//对minCapacity和MAX_ARRAY_SIZE进行比较
//若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
//若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}这个方法就是最后检查一下扩容后的最小容量
minCapacity
是否真的需要那么大的容量;最后将旧数组
elementData
复制到新容量newCapacity
大小的新数组中,完成扩容。这一操作是调用Arrays.copyOf()
完成的:1
2
3
4
5
6public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}Arrays.copyOf()
有各种类型参数的重载方法,其底层就是调用了System.arraycopy()
这个 native 方法。在 ArrayList 中大量的调用Arrays.copyOf()
和System.arraycopy()
,其中前文说的添加/删除
时元素移动操作就是借助System.arraycopy()
方法实现的。
ensureCapacity()
ArraysList 中提供了一个用户方法ensureCapacity()
:
1 | /** |
这个方法就是提供给用户主动进行容量检查的,如果此时 ArrayList 实例的数组容量小于传参var1
则会进行扩容操作。
PS:在使用 ArrayList 时,如果要添加大量元素,最好先调用ensureCapacity()
方法,从而减少 add 方法中重新分配容量的次数,这样会有效提升添加操作的执行速度。
本人菜鸟,有错误请告知,感激不尽!
更多题解和源码:github