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