背景
我们都知道linkedlist底层是基于链表实现,ArrayList是基于数组实现。
而使用他们的场合分别有顺序插入,随机插入,顺序读取,随机读取,删除五大常见场景。
五个场景的比较
- 随机插入:从底层实现原理上,linkedList明显优于arrayList,但是实际场景中我们难以见到随机插入的情况。同时,在随机插入的结构上,我们有更好的选择,那就是map结构和set结构。都比linkedList性能要好。
- 顺序读取,随机读取,毫无疑问,基于数组的ArrayList更优秀
- 删除:这是linkedList唯一胜出的地方,那就是队列。只有做队列时,我们才会优先考虑LinkeList
接下来是顺序插入这个常见误区的比较了
两者插入的区别-扩容机制
因为我们知道数组是定长的,为了实现不定长的list。底层实现了一套动态扩容机制。
Java中的ArrayList
在以下情况会扩容:
- 当通过
add()
方法添加元素,且当前数组容量(capacity)不足以容纳新的元素时。 - 执行
addAll()
方法添加多个元素,如果添加后总元素数量超过现有容量。
ArrayList的底层扩容机制如下:
- 默认初始容量:当创建一个空的ArrayList时,默认容量为10。
- 扩容检查:每次调用
add()
等增加元素的方法前,都会调用ensureCapacityInternal()
方法来确保有足够的空间存放新元素。若当前容量不足,则会进行扩容操作。 - 扩容策略:扩容时,ArrayList通常不是仅仅增加一单位容量,而是按照一定比例扩大容量。在JDK 1.8及之后版本中,扩容的具体实现是将容量翻倍(即新容量 = 原容量 * 2),然后向上取整到大于等于最小需要容量的数值。
- 具体扩容过程:
- 创建一个新的数组,大小为新计算出的容量。
- 将原数组中的所有元素复制到新数组中。
- 更新内部数据结构,使得ArrayList引用新的、更大容量的数组。
扩容是一个相对耗资源的操作,因为它涉及到内存分配和数组元素的拷贝。因此,在预知 ArrayList 大致规模的情况下,可以预先通过ensureCapacity(int minCapacity)
方法手动指定一个初始容量或期望的容量,以减少扩容次数。
常见误区
而且因为arrayList底层是数组,那么因为数组是定长的。那么一旦达到阈值,就会让数组进行扩容。那么因为arrayList多了一个扩容的操作。许多人认为arraylist不适合顺序插入。事实正好相反。以下为测试代码
public void testList(List<Integer> list, String logPre, int loopCount) {
long startMill = System.currentTimeMillis();
for (int i = 0; i < loopCount; i++) {
list.add(i);
}
long endMill = System.currentTimeMillis();
double value = endMill - startMill;
System.out.printf("%s loopCount:%s costSec:%s\n", logPre, loopCount, value / 1000);
}
@Test
public void testLinkedList() {
testList(new LinkedList<>(), "linkedList", 10);
testList(new LinkedList<>(), "linkedList", 100);
testList(new LinkedList<>(), "linkedList", 10000);
testList(new LinkedList<>(), "linkedList", 1000000);
testList(new LinkedList<>(), "linkedList", 100000000);
testList(new LinkedList<>(), "linkedList", Integer.MAX_VALUE / 20);
}
@Test
public void testArrayList() {
testList(new ArrayList<>(), "arrayList", 10);
testList(new ArrayList<>(), "arrayList", 100);
testList(new ArrayList<>(), "arrayList", 10000);
testList(new ArrayList<>(), "arrayList", 1000000);
testList(new ArrayList<>(), "arrayList", 100000000);
testList(new ArrayList<>(), "arrayList", Integer.MAX_VALUE / 20);
}
运行结果
arrayList loopCount:10 costSec:0.0
arrayList loopCount:100 costSec:0.0
arrayList loopCount:10000 costSec:0.002
arrayList loopCount:1000000 costSec:0.259
arrayList loopCount:100000000 costSec:3.053
arrayList loopCount:107374182 costSec:3.397
linkedList loopCount:10 costSec:0.0
linkedList loopCount:100 costSec:0.0
linkedList loopCount:10000 costSec:0.0
linkedList loopCount:1000000 costSec:0.007
linkedList loopCount:100000000 costSec:4.817
linkedList loopCount:107374182 costSec:23.747
随着数量的增多。linkedList的插入越发的花费时间。
以上的结果让我很吃惊。只能说明动态扩容其实不怎么消耗所谓的时间,代价非常小。因为每次扩容都是批量式的开辟内存空间。而linkedList这是因为在内存中是分散的,每次都需要jvm重新开辟内存。
总结
由上面的测试可知,除了队列场景之外,都应该优先选择ArrayList来作为队列存储的实现。
如果涉及到大量的随机插入的情况,可以考虑使用树结构。