ArrayList和LinkedList优缺点对比,实际与你背的不一样!

arraylist和linkedlist有什么特点?我相信基本准备过或者说学习过的人应该都对答如流吧,底层实现,数据结构,数组,链表,查找效率,增删效率等等,这些基本上搜索引擎可以随便找到,而且基本上所有的文章差不多都是那点儿货,大家也把这些东西奉若真理,人云亦云,其实只需要非常简单的代码就可以测试这个结论到底对不对。

实现原理

简单的说一下实现原理吧,有助于下面内容的展开,简单带过:ArrayList与LinkedList都是List接口实现类

  • ArrayList:
    虽然叫list,底层是通过动态数组来实现,也就是一块连续的内存,初始化默认大小是10,每次扩容为1.5倍

  • LinkedList
    java中的链表,内部定义了Node类,类中item用于存储数据,两个Node类分别为prev和next来实现双向链表,每次新增元素时new Node()

目前熟知的优缺点

  • ArrayList
    优点:由于底层实现是数组,查找效率为O(1)
    缺点:插入和删除操作需要移动所有受影响的元素,效率相对较低

  • LinkedList
    优点:插入和删除时只需要添加或删除前后对象的引用,插入较快
    缺点:在内存中存储不连续,只能通过遍历查询,效率相对较低

测试

首先由于数组的特性,所以插入操作的话我们分为三种情况:从头部插入,从中间插入和从尾部插入

**测试结果肯定机器性能和当时运行状况的影响,由于只是简单的测试,并不需要非常复杂严谨的测试策略,我这里基本也就是一个情况跑5~10次,而且代码也只是简单的单线程循环插入操作,大体测试结果和趋势是没问题的

 1  @Test
 2     public void addFromHeaderTestArray() {
 3         ArrayList<Integer> list = new ArrayList<>();
 4         int i = 0;
 5 
 6         long timeStart = System.nanoTime();
 7 
 8         while (i < 100) {
 9             list.add(0, i);
10             i++;
11         }
12         long timeEnd = System.nanoTime();
13         System.out.println("ArrayList cost" + (timeEnd - timeStart));
14     }
15 
16     @Test
17     public void addFromHeaderTestLinked() {
18         LinkedList<Integer> list = new LinkedList<>();
19         int i = 0;
20         long timeStart = System.nanoTime();
21         while (i < 100) {
22             list.addFirst(i);
23             i++;
24         }
25         long timeEnd = System.nanoTime();
26 
27         System.out.println("LinkedList cost" + (timeEnd - timeStart));
28     }

 

代码比较简单,只贴出来一段了,每次改一下循环次数和add方法就可以了,以下时间单位均为纳秒

  • 头部插入
    测试结果(100条):
name/times12345
ArrayList 88300 84900 101700 93800 88100
LinkedList 68400 71200 88600 93300 96000

只测试了100条的情况,结果符合预期,这里我特意放了一条特殊情况,因为我们的实验确实比较简单不够复杂和系统,结果跟当时电脑的运行状况有关,但是我们这里只是插入100条,如果你换成10000条,你就会发现差距相当明显了,而且不管跑多少次,都不会出现ArrayList更快的情况,这里具体结果不贴了,有兴趣自己跑一下,结论是没问题的。

  • 中间插入
1    while (i < 100) {
2             int temp = list.size();
3             list.add(temp / 2, i);
4             i++;
5         }

测试结果(100条):

|name/times|1|2|3|4|5|
|—|
|ArrayList|128300|92800|106300|77600|90700|
|LinkedList|175100|210900|164200|164200|195700|

测试结果(10000条):

name/times1234
ArrayList 9745300 10319900 10986800 11696600
LinkedList 66968400 63269400 70954900 65432600

这次中间插入分别测试了100条和10000条,是不是有点儿慌了?怎么和自己学的不一样了?从中间插入居然ArrayList更快?

  • 尾部插入
1   while (i < 10000) {
2            list.add(i);
3            i++;
4        }

测试结果(100条):

name/times12345
ArrayList 32100 23600 23500 27800 16100
LinkedList 72200 73400 70200 74800 90000

结果已经很明显了,只贴了100条的数据,有兴趣可以自己试试,插入更多条也还是ArrayList更快,怎么样?看到这里是不是颠覆认知了

测试结果
头部插入:ArrayList>LinkedList
中间插入:ArrayList<LinkedList
尾部插入:ArrayList<LinkedList (差不多,AL稍微快一点;对于尾部插入而言,ArrayList 与 LinkedList 的性能几乎是一致的。)

结果分析

  • 头部插入:由于ArrayList头部插入需要移动后面所有元素,所以必然导致效率低
  • 中间插入:查看源码会注意到LinkedList的中间插入其实是先判断插入位置距离头尾哪边更接近,然后从近的一端遍历找到对应位置,而ArrayList是需要将后半部分的数据复制重排,所以两种方式其实都逃不过遍历的操作,相对效率都很低,但是从实验结果我们可以看到还是ArrayList更胜一筹,我猜测这与数组在内存中是连续存储有关
  • 尾部插入:ArrayList并不需要复制重排数据,所以效率很高,这也应该是我们日常写代码时的首选操作,而LinkedList由于还需要new对象和变换指针,所以效率反而低于ArrayList

删除操作这里不做实验了,但是可以明确的告诉大家,结果是一样的,因为逻辑和添加操作并没有什么区别

数组扩容
再说一下数组扩容的问题,虽然ArrayList存在扩容的效率问题,但这只是在容量较小的时候,假如初始是10,第一次扩容结束是15,没有增加太多,但是如果是10000,那么扩容后就到了15000,实际上越往后每次扩容对后续性能影响越小,而且即便存在扩容问题,实验结果表明还是优于LinkedList的。

 

 

LinkedList源码

 1 // 在index前添加节点,且节点的值为element
 2 public void add(int index, E element) {
 3     addBefore(element, (index==size ? header : entry(index)));
 4 }
 5 
 6 // 获取双向链表中指定位置的节点
 7 private Entry<E> entry(int index) {
 8     if (index < 0 || index >= size)
 9         throw new IndexOutOfBoundsException("Index: "+index+
10                                             ", Size: "+size);
11     Entry<E> e = header;
12     // 获取index处的节点。
13     // 若index < 双向链表长度的1/2,则从前向后查找;
14     // 否则,从后向前查找。
15     if (index < (size >> 1)) {
16         for (int i = 0; i <= index; i++)
17             e = e.next;
18     } else {
19         for (int i = size; i > index; i--)
20             e = e.previous;
21     }
22     return e;
23 }
24 
25 // 将节点(节点数据是e)添加到entry节点之前。
26 private Entry<E> addBefore(E e, Entry<E> entry) {
27     // 新建节点newEntry,将newEntry插入到节点e之前;并且设置newEntry的数据是e
28     Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
29     // 插入newEntry到链表中
30     newEntry.previous.next = newEntry;
31     newEntry.next.previous = newEntry;
32     size++;
33     modCount++;
34     return newEntry;

 

从中,我们可以看出:通过add(int index, E element)向LinkedList插入元素时。先是在双向链表中找到要插入节点的位置index;找到之后,再插入一个新节点
双向链表查找index位置的节点时,有一个加速动作若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找

接着,我们看看ArrayList.java中向指定位置插入元素的代码。如下:

 1 // 将e添加到ArrayList的指定位置
 2 public void add(int index, E element) {
 3     if (index > size || index < 0)
 4         throw new IndexOutOfBoundsException(
 5         "Index: "+index+", Size: "+size);
 6 
 7     ensureCapacity(size+1);  // Increments modCount!!
 8     System.arraycopy(elementData, index, elementData, index + 1,
 9          size - index);
10     elementData[index] = element;
11     size++;
12 }

ensureCapacity(size+1) 的作用是“确认ArrayList的容量,若容量不够,则增加容量。
真正耗时的操作是 System.arraycopy(elementData, index, elementData, index + 1, size - index);

 

实际上,我们只需要了解: System.arraycopy(elementData, index, elementData, index + 1, size - index); 会移动index之后所有元素即可这就意味着,ArrayList的add(int index, E element)函数,会引起index之后所有元素的改变!

插入位置的选取对LinkedList有很大的影响,一直往数据中间部分与尾部插入删除的时候,ArrayList比LinkedList更快

原因大概就是当数据量大的时候,system.arraycopy的效率要比每次插入LinkedList都需要从两端查找index和分配节点node来的更快。

LinkedList 双向链表实现

这里写图片描述

可以看到, LinkedList 的成员变量只有三个:

  • 头节点 first
  • 尾节点 last
  • 容量 size

节点是一个双向节点: 
这里写图片描述

用一副图表示节点:

这里写图片描述

双向链表的底层结构图:

这里写图片描述

每个节点元素里包含三部分:前一个节点元素地址,下一个节点元素地址,当前节点元素内容

LinkedList 特点

  • 双向链表实现
  • 元素时有序的,输出顺序与输入顺序一致
  • 允许元素为 null
  • 所有指定位置的操作都是从头开始遍历进行的
  • 和 ArrayList 一样,不是同步容器

并发访问注意事项

linkedList 和 ArrayList 一样,不是同步容器。所以需要外部做同步操作,或者直接用 Collections.synchronizedList 方法包一下,最好在创建时就包一下:

List list = Collections.synchronizedList(new LinkedList(...));

 

Thanks:

https://blog.csdn.net/zycR10/article/details/99775821?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

https://www.cnblogs.com/syp172654682/p/9817277.html

深入理解linkedlist: https://blog.csdn.net/u011240877/article/details/52876543

原文地址:https://www.cnblogs.com/shenjiangwei/p/13769370.html