• 27、ArrayList和LinkedList的区别


    在Java的List类型集合中,ArrayList和LinkedList大概是最常用到的2个了,细看了一下它们的实现,发现区别还是很大的,这里简单的列一下个人比较关心的区别。

    类声明

    ArrayList

    public class ArrayList<E>extends AbstractList<E>implements List<E>,RandomAccess, Cloneable, java.io.Serializable

    LinkedList

    public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable

    二者的定义有些相近,除了都实现List、Cloneable和Serializable以外,继承的类不一样,以及接口有细微的区别。

    public abstract class AbstractSequentialList<E> extends AbstractList<E>

    AbstractSequentialList也继承自AbstractList,它只是多了一些实现的方法,参照API的doc,这个类用于按顺序访问的List的实现,所谓顺序访问(sequential access),可以与随即访问(random access)的ArrayList对比去理解。

    Deque是一个双向(double ended queue)的Queue的接口,因为这个接口的区别,LinkedList里实现的方法要比ArrayList多一些。

    元素存储方式

    ArrayList:采用数组方式

    private transient Object[] elementData;

    LinedList:采用链表

     1 private transient Entry<E> header = new Entry<E>(null, null, null);
     2 private static class Entry<E> {
     3     E element;
     4     Entry<E> next;
     5     Entry<E> previous;
     6     Entry(E element, Entry<E> next, Entry<E> previous) {
     7         this.element = element;
     8         this.next = next;
     9         this.previous = previous;
    10     }
    11 }

    很好理解,从字面都可以理解出来,一个是数组实现,一个是链表实现。

    元素添加

    二者都有几个add()方法,

    void add(E item)  向滚动列表的末尾添加指定的项。 void add(E item, int index)  向滚动列表中索引指示的位置添加指定的项。

    先看看ArrayList的实现:

     1 public void add(int index, E element) {
     2     if (index > size || index < 0)
     3         throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
     4     ensureCapacity(size+1);  // Increments modCount!!
     5     System.arraycopy(elementData, index, elementData, index + 1,size - index);
     6     elementData[index] = element;
     7     size++;
     8 }
     9  
    10 public boolean add(E e) {
    11     ensureCapacity(size + 1);  // Increments modCount!!
    12     elementData[size++] = e;
    13     return true;
    14 }


    对于add(E e)方法,非常简单,首先确保数组容量,然后直接赋值。在不需要扩充数组容量的情况下,效率非常高,而一旦需要数组扩容,代价就会上升:

     1 public void ensureCapacity(int minCapacity) {
     2     modCount++;
     3     int oldCapacity = elementData.length;
     4     if (minCapacity > oldCapacity) {
     5         Object oldData[] = elementData;
     6         int newCapacity = (oldCapacity * 3)/2 + 1;
     7         if (newCapacity < minCapacity)
     8             newCapacity = minCapacity; // minCapacity is usually close to size, so this is a win:
     9         elementData = Arrays.copyOf(elementData, newCapacity);
    10     }
    11 }

    因为它需要将已有的数组复制到新的数组里去。由此便可以想到一个提高add()效率的方法,在一开始尽量设定一个合理的数组容量,那么可以有效地减少数组的扩容和大量的复制。

    对于add(int index, E e),比起add(E e),多一个可能的复制操作,这样才能保证在合理的位置插入新的元素。

    LinkedList的实现:

     1 public boolean add(E e) {
     2     addBefore(e, header);
     3     return true;
     4 }
     5 
     6 private Entry<E> addBefore(E e, Entry<E> entry) {
     7     Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
     8     newEntry.previous.next = newEntry;
     9     newEntry.next.previous = newEntry;
    10     size++;
    11     modCount++;
    12     return newEntry;
    13 }
    14 
    15 public void add(int index, E element) {
    16     addBefore(element, (index==size ? header : entry(index)));
    17 }
    18 
    19 private Entry<E> entry(int index) {
    20     if (index < 0 || index >= size)
    21         throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
    22     Entry<E> e = header;
    23     if (index < (size >> 1)) {
    24         for (int i = 0; i <= index; i++)
    25             e = e.next;
    26     } else {
    27         for (int i = size; i > index; i--)
    28             e = e.previous;
    29     }
    30     return e;
    31 }


    粗略看起来要复杂一些,因为LinkedList同时还是一个Deque(JDK 1.6新添加的),所以它的实现也要兼顾双向队列。

    下面从一个空的LinkedList开始,看看新的元素是如何添加进来的:

    1 List<Integer> ints = new LinkedList<Integer>();
    2 ints.add(1);
    3 ints.add(2);
    4 ints.add(3);
    5 System.out.println(ints); //[1, 2, 3]

    下面一步一步看List内部header和元素之间的关系:

    • 初始化: header.element = null; header.next=header.previous=header 这里是一个环状的结构,自己的p和n指针都指向自己

    • 添加第一个元素“1”:header.element=null;header.next=1;header.previous=1; 2个元素相互连接
    • 添加第二个元素“2” 这里很明显看来了,是一个环状结构
    • 添加第三个元素“3” 既然是一个环状,干脆用圆形显示好了,貌似画的不太圆。。。

    这里总结一下两种的差别:

    • 对于元素的add()来说,LinkedList要比ArrayList要快一些,因为ArrayList可能需要额外的扩容操作,当然如果没有扩容,二者没有很大的差别
    • 对于元素的add(int, element),对于LinkedList来说,代价主要在遍历获取插入的位置的元素,而ArrayList的主要代价在于可能有额外的扩容和大量元素的移动
    • 小结:对于简单的元素添加,如果事先知道元素的个数,采用预置大小的ArrayList要更好,反之可以考虑LinkedList

    元素移除

    ArrayList的元素移除:

     1 public E remove(int index) {
     2     RangeCheck(index);
     3     modCount++;
     4     E oldValue = (E) elementData[index];
     5     int numMoved = size - index - 1;
     6     if (numMoved > 0)
     7         System.arraycopy(elementData, index+1, elementData, index,numMoved);
     8     elementData[--size] = null; // Let gc do its work
     9     return oldValue;
    10 }
    11 
    12 public boolean remove(Object o) {
    13     if (o == null) {
    14         for (int index = 0; index < size; index++)
    15             if (elementData[index] == null) {
    16                 fastRemove(index);
    17                 return true;
    18             }
    19     } else {
    20         for (int index = 0; index < size; index++)
    21             if (o.equals(elementData[index])) {
    22                 fastRemove(index);
    23                 return true;
    24             }
    25         }
    26 
    27     return false;
    28 
    29 }
    30 
    31 /*
    32 
    33  * Private remove method that skips bounds checking and does not
    34 
    35  * return the value removed.
    36 
    37  */
    38 
    39 private void fastRemove(int index) {
    40     modCount++;
    41     int numMoved = size - index - 1;
    42     if (numMoved > 0)
    43         System.arraycopy(elementData, index+1, elementData, index,numMoved);
    44     elementData[--size] = null; // Let gc do its work
    45 }

    remove(int)和remove(Object)两种方式的返回值是有区别的哦

    对于ArrayList来说,主要是的仍然会有元素的移动(这里就是数组的复制),虽然采用的是System的arrayCopy,但是本质上还是复制的思路。还有一点需要注意的是,remove(Object)对null值进行单独处理,这里也说明ArrayList是可以存取null的。

    LinkedList元素移除:

     
     1 public E remove(int index) {
     2 
     3      return remove(entry(index));
     4 
     5  }
     6 
     7  
     8 
     9  /**
    10 
    11   * Returns the indexed entry.
    12 
    13   */
    14 
    15  private Entry<E> entry(int index) {
    16 
    17      if (index < 0 || index >= size)
    18 
    19          throw new IndexOutOfBoundsException("Index: "+index+
    20 
    21                                              ", Size: "+size);
    22 
    23      Entry<E> e = header;
    24 
    25      if (index < (size >> 1)) {
    26 
    27          for (int i = 0; i <= index; i++)
    28 
    29              e = e.next;
    30 
    31      } else {
    32 
    33          for (int i = size; i > index; i--)
    34 
    35              e = e.previous;
    36 
    37      }
    38 
    39      return e;
    40 
    41  }
    42 
    43  
    44 
    45 public boolean remove(Object o) {
    46 
    47      if (o==null) {
    48 
    49          for (Entry<E> e = header.next; e != header; e = e.next) {
    50 
    51              if (e.element==null) {
    52 
    53                  remove(e);
    54 
    55                  return true;
    56 
    57              }
    58 
    59          }
    60 
    61      } else {
    62 
    63          for (Entry<E> e = header.next; e != header; e = e.next) {
    64 
    65              if (o.equals(e.element)) {
    66 
    67                  remove(e);
    68 
    69                  return true;
    70 
    71              }
    72 
    73          }
    74 
    75      }
    76 
    77      return false;
    78 
    79  }

    这里的实现就是典型的链表删除的实现,其中有几个细节需要提一下:

    • modCount的处理,这个变量是用来存储List的修改的次数的,仅仅存储添加和删除的操作此书,用来在Iterator中判断List的状态和行为,防止不同步的修改,抛出ConcurrentModificationException
    • 通过索引访问元素的实现entry(int),这里有一个小细节,
       if (index < (size >> 1)) {

    如果元素的位置在前半段,那么通过next指针查找,否则通过previous指针查找。这一行代码有2个值得学习的地方,第一查找的优化,根据位置判断查找的方向,第二移位操作的运用。不得不佩服Bloch的编程功底。

    小结一下:

    删除操作中,LinkedList更有优势,一旦找到了删除的节点,它仅仅只是断开链接关系,并没有元素复制移动的行为,而ArrayList不可避免的又要进行元素的移动。

    元素索引

    indexOf(Object o)  回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1。

    ArrayList的实现:

     1 public int indexOf(Object o) {
     2     if (o == null) {
     3         for (int i = 0; i < size; i++)
     4         if (elementData[i]==null)
     5             return i;
     6 
     7     } else {
     8             for (int i = 0; i < size; i++)
     9 
    10                 if (o.equals(elementData[i]))
    11 
    12                         return i;
    13 
    14     }
    15     return -1;
    16 }

    LinkedList的实现:

     1 public int indexOf(Object o) {
     2 
     3     int index = 0;
     4 
     5     if (o==null) {
     6 
     7         for (Entry e = header.next; e != header; e = e.next) {
     8 
     9             if (e.element==null)
    10 
    11                 return index;
    12 
    13             index++;
    14 
    15         }
    16 
    17     } else {
    18 
    19         for (Entry e = header.next; e != header; e = e.next) {
    20 
    21             if (o.equals(e.element))
    22 
    23                 return index;
    24 
    25             index++;
    26 
    27         }
    28 
    29     }
    30 
    31     return -1;
    32 
    33 }

    ArrayList:基于数组的遍历查找

    LinkedList:基于链表的遍历查找

    按照对象在内存中存储的顺序去考虑,数组的访问要比链接表快,因为对象都存储在一起。

    遍历

    基于以上的分析,可以得出,按照索引遍历,ArrayList是更好的选择,按照Iterator遍历,也许LinkedList会好一些。

    反过来理解,如果是ArrayList,Iterator和index遍历都可以,如果是LinkedList,优先选择Iterator比较好。

  • 相关阅读:
    关于缓存雪崩穿透击穿等一些问题
    MethodHandler笔记
    并发总结(博客转载)
    负载均衡的几种算法Java实现代码
    SpringJdbc插入对象返回主键的值
    【Java基础】01-推荐参考材料
    【Java基础】注解
    【JSON】
    【Kafka】3-配置文件说明
    【Kafka】1-理论知识
  • 原文地址:https://www.cnblogs.com/caoyc/p/4603882.html
Copyright © 2020-2023  润新知