Arraylist 与 LinkedList 异同

一、是否保证线程安全

  ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全,相对线程安全的Vector,执行效率高。

二、底层数据结构

  (1)Arraylist 底层使⽤的是 Object 数组,基于动态数组的数据结构。
  (2)LinkedList 底层使⽤的是 双向链表 数据结构(JDK1.6 之前为双向循环链表,JDK1.7 取消了循环,变为双向链表。注意双向链表和双向循环链表的区别)

三、插⼊和删除是否受元素位置的影响

  ① ArrayList 采⽤数组存储,所以插⼊和删除元素的时间复杂度受元素位置的影响。
  ⽐如:执⾏ add(E e) ⽅法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。
  但是如果要在指定位置 i 插⼊和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。
  因为在进⾏上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执⾏向后位/向前移⼀位的操作。
 
  ② LinkedList 采⽤链表存储,所以对于 add(E e) ⽅法的插⼊,删除元素时间复杂度不受元素位置的影响,近似 O(1)。
  如果是要在指定位置 i 插⼊和删除元素的话( (add(int index, E element) ) 时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插⼊。
 
  对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
  对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。

四、是否⽀持快速随机访问

  LinkedList 不⽀持⾼效的随机元素访问,而 ArrayList ⽀持。
  快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法)。ArrayList 是基于数组实现的,可以通过下标随机访问元素,并且 ArrayList 还具有
RandomAccess 接⼝,标识实现这个接⼝的类具有随机访问功能。

五、内存空间占用

  ArrayList 的空间浪费主要体现在 list 列表的结尾会预留⼀定的容量空间,⽽ LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间
(因为要存放直接后继和直接前驱以及数据)。

  1、ArrayList

ArrayList是List接口的一个实现类,底层是基于数组实现的存储结构,可以用于装载数据,数据都是存放到一个数组变量中,
transient Object[] elementData; 

  transient是一个关键字,它的作用可以总结为一句话:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。你可能会觉得奇怪,ArrayList可以被序列化的啊,源码可是实现了java.io.Serializable接口啊,为什么数组变量还要用transient定义呢?

  先埋个伏笔,后面揭晓。

  当我们新建一个实例时,ArrayList会默认帮我们初始化数组的大小为10

/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

  但请注意,这个只是数组的容量大小,并不是List真正的大小,List的大小应该由存储数据的数量决定,在源码中,获取真实的容量其实是用一个变量size来表示,

private int size;

  在源码中,数据默认是从数组的第一个索引开始存储的,当我们添加数据时,ArrayList会把数据填充到上一个索引的后面去,所以,ArrayList的数据都是有序排列的。而且,由于ArrayList本身是基于数组存储,所以查询的时候只需要根据索引下标就可以找到对于的元素,查询性能非常的高,这也是我们非常青睐ArrayList的最重要的原因。

   

 

  但是,数组的容量是确定的啊,如果要存储的数据大小超过了数组大小,那不就有数组越界的问题?

  关于这点,我们不用担心,ArrayList帮我们做了动态扩容的处理,如果发现新增数据后,List的大小已经超过数组的容量的话,就会新增一个为原来1.5倍容量的新数组,然后把原数组的数据原封不动的复制到新数组中,再把新数组赋值给原来的数组对象就完成了。

  

 

  扩容之后,数组的容量足够了,就可以正常新增数据了。

  除此之外,ArrayList提供支持指定index新增的方法,就是可以把数据插入到设定的索引下标,比如说我想把元素4插入到3后面的位置,也就是现在5所在的地方,

  

 

   插入数据的时候,ArrayList的操作是先把3后面的数组全部复制一遍,然后将这部分数据往后移动一位,其实就是逐个赋值给后移一位的索引位置,然后3后面就可以空出一个位置,把4放入就完成了插入数据的操作了

   

 

  删除的时候也是一样,指定index,然后把后面的数据拷贝一份,并且向前移动,这样原来index位置的数据就删除了。

  到这里我们也不难发现,这种基于数组的查询虽然高效,但增删数据的时候却很耗性能,因为每增删一个元素就要移动对应index后面的所有元素,数据量少点还无所谓,但如果存储上千上万的数据就很吃力了,所以,如果是频繁增删的情况,不建议用ArrayList。

  既然ArrayList不建议用的话,这种情况下有没有其他的集合可用呢?

 

  2、LinkedList

  LinkedList 是基于双向链表实现的,不需要指定初始容量,链表中任何一个存储单元都可以通过向前或者向后的指针获取到前面或者后面的存储单元。在 LinkedList 的源码中,其存储单元用一个Node类表示:
 1 private static class Node<E> {
 2     E item;
 3     Node<E> next;  
 4     Node<E> prev;
 5 
 6     Node(Node<E> prev, E element, Node<E> next) {
 7         this.item = element;
 8         this.next = next;
 9         this.prev = prev;
10     }
11 }

 

  Node中包含了三个成员,分别是存储数据的item,指向前一个存储单元的点 prev 和指向后一个存储单元的节点 next ,通过这两个节点就可以关联前后的节点,组装成为链表的结构:

  

   因为有保存前后节点的地址,LinkedList增删数据的时候不需要像ArrayList那样移动整片的数据,只需要通过引用指定index位置前后的两个节点即可,比如我们要在李白和韩信之间插入孙悟空的节点,只需要像这样处理下节点之间的指向地址:

  

 

  删除数据也是同样原理,只需要改变index位置前后两个节点的指向地址即可。

  这样的链表结构使得LinkedList能非常高效的增删数据,在频繁增删的情景下能很好的使用,但不足之处也是有的。

  虽然增删数据很快,但查询就不怎么样了,LinkedList是基于双向链表存储的,当查询对应index位置的数据时,会先计算链表总长度一半的值,判读index是在这个值的左边还是右边,然后决定从头结点还是从尾结点开始遍历,

 1   Node<E> node(int index) {
 2         // assert isElementIndex(index);
 3 
 4         if (index < (size >> 1)) {
 5             Node<E> x = first;
 6             for (int i = 0; i < index; i++)
 7                 x = x.next;
 8             return x;
 9         } else {
10             Node<E> x = last;
11             for (int i = size - 1; i > index; i--)
12                 x = x.prev;
13             return x;
14         }
15     }

 

  虽然已经二分法来做优化,但依然会有遍历一半链表长度的情况,如果是数据量非常多的话,这样的查询无疑是非常慢的。

  这也是LinkedList最无奈的地方,鱼和熊掌不可兼得,我们既想查的快,又想增删快,这样的好事怎么可能都让我们遇到呢?所以,一般建议LinkedList使用于增删多,查询少的情景。

  除此之外,LinkedList对内存的占用也是比较大的,毕竟每个Node都维护着前后指向地址的节点,数据量大的话会占用不少内存空间。

  

  3、两者哪个更占空间?

    表面上看,LinkedList的Node存储结构似乎更占空间,但别忘了前面介绍ArrayList扩容的时候,它会默认把数组的容量扩大到原来的1.5倍的,如果你只添加一个元素的话,那么会有将近原来一半大小的数组空间被浪费了,如果原先数组很大的话,那么这部分空间的浪费也是不少的,

    所以,如果数据量很大又在实时添加数据的情况下,ArrayList占用的空间不一定会比LinkedList空间小,这样的回答就显得谨慎些了。

    还记得前面说的那个 transient 变量吗?它的作用已经说了,不想序列化的对象就可以用它来修饰,用 transient 修饰elementData意味着我不希望elementData数组被序列化。为什么要这么做呢?

    这是因为序列化ArrayList的时候,ArrayList里面的 elementData,也就是数组未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:

 1 private void writeObject(java.io.ObjectOutputStream s)
 2     throws java.io.IOException{
 3     // Write out element count, and any hidden stuff
 4     int expectedModCount = modCount;
 5     s.defaultWriteObject();
 6 
 7     // Write out size as capacity for behavioural compatibility with clone()
 8     s.writeInt(size);
 9 
10     // Write out all elements in the proper order.
11     for (int i=0; i<size; i++) {
12         s.writeObject(elementData[i]);
13     }
14 
15     if (modCount != expectedModCount) {
16         throw new ConcurrentModificationException();
17     }
18 }

 

  每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData这个数组对象不去序列化它,而是遍历elementData,只序列化数组里面有数据的元素,这样一来,就可以加快序列化的速度,还能够减少空间的开销。

加上这个知识点后,我们对上面那个问题就可以有更加全面的回答了,如果你下次也遇到这个问题的话,你可以参考一下我的说法:

  一般情况下,LinkedList的占用空间更大,因为每个节点要维护指向前后地址的两个节点,但也不是绝对,如果刚好数据量超过ArrayList默认的临时值时,ArrayList占用的空间也是不小的,因为扩容的原因会浪费将近原来数组一半的容量,不过,因为ArrayList的数组变量是用transient关键字修饰的,如果集合本身需要做序列化操作的话,ArrayList这部分多余的空间不会被序列化。

 

posted on 2021-04-19 10:41  格物致知_Tony  阅读(160)  评论(0编辑  收藏  举报