在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比较好。

 

 posted on 2015-06-27 11:57  Just_Do  阅读(419)  评论(0编辑  收藏  举报