為方便開發人員,JDK提供了一套主要數據結構的實現,比如List、Map等。今兒說說List接口。
List接口的一些列實現中,最常用最重要的就是這三個:ArrayList、Vector、LinkedList。
JDK中這三個類的定義:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
從這三個類定義就可以看出一些信息:
- 三個都直接實現了AbstractList這個抽象類
- ArrayList和Vector都實現了RandomAccess接口,而LinkedList沒有,這是什么意思呢?在JDK中,RandomAccess接口是一個空接口,所以它沒有實際意義,就是一個標記,標記這個類支持快速隨機訪問,所以,arrayList和vector是支持隨機訪問的,但是LinkedList不支持
- serializbale接口表名,他們都支持序列化
下面詳細說說這三個List實現。
這三個里面,ArrayList和Vector使用了數組的實現,相當於封裝了對數組的操作。這也正是他們能夠支持快速隨機訪問的原因,多說一句,JDK中所有基於數組實現的數據結構都能夠支持快速隨機訪問。
ArrayList和Vector的實現上幾乎都使用了相同的算法,他們的主要區別就是ArrayList沒有對任何一個方法做同步,所以不是線程安全的;而Vector中大部分方法都做了線程同步,是線程安全的。
LinkedList使用的是雙向循環鏈表的數據結構。由於是基於鏈表的,所以是沒法實現隨機訪問的,只能順序訪問,這也正式它沒有實現RandomAccess接口的原因。
正式由於ArrayList、Vector和LinkedList所采用的數據結構不同,注定他們適用的是完全不同的場景。
通過閱讀這幾個類的源碼,我們可以看到他們實現的不同。ArrayList和Vector基本一樣,我們就拿ArrayList和LinkedList做對比。
在末尾增加一個元素
ArrayList中的add方法實現如下:
1 public boolean add(E e) { 2 ensureCapacityInternal(size + 1); // Increments modCount!! 3 elementData[size++] = e; 4 return true; 5 }
這個方法做兩件事情,首先確保數組空間足夠大,然后在數組末尾增加元素並且通過后++使得完成size+1。
從這個代碼可以看出,如果數組空間足夠大,那么只是數組的add操作就是O(1)的性能,非常高效。
在看看ensureCapacityInternal這個方法的實現:
1 private void ensureCapacityInternal(int minCapacity) { 2 modCount++; 3 // overflow-conscious code 4 if (minCapacity - elementData.length > 0) 5 grow(minCapacity); 6 } 7 8 private void grow(int minCapacity) { 9 // overflow-conscious code 10 int oldCapacity = elementData.length; 11 int newCapacity = oldCapacity + (oldCapacity >> 1); 12 if (newCapacity - minCapacity < 0) 13 newCapacity = minCapacity; 14 if (newCapacity - MAX_ARRAY_SIZE > 0) 15 newCapacity = hugeCapacity(minCapacity); 16 // minCapacity is usually close to size, so this is a win: 17 elementData = Arrays.copyOf(elementData, newCapacity); 18 }
可以看出,如果數組空間不夠,那么這個方法就會做數組擴容和數組復制操作,看第11行,JDK利用移位運算符進行擴容計算,>>1右移一位表示除2,所以newCapacity就是擴容為原來的1.5倍。
PS:這里的代碼都是JDK1.7中的實現,JDK1.7對1.6的很多代碼做了優化,比如上面這段擴容代碼,在JDK1.6中第11行是直接除2,顯然,移位運算要更高效。
在看看LinkedList中的add方法:
1 public boolean add(E e) { 2 linkLast(e); 3 return true; 4 } 5 6 void linkLast(E e) { 7 final Node<E> l = last; 8 final Node<E> newNode = new Node<>(l, e, null); 9 last = newNode; 10 if (l == null) 11 first = newNode; 12 else 13 l.next = newNode; 14 size++; 15 modCount++; 16 }
1 Node(Node<E> prev, E element, Node<E> next) { 2 this.item = element; 3 this.next = next; 4 this.prev = prev; 5 }
從這段add代碼可以看出,LinkedList由於使用了鏈表,所以不需要進行擴容,直接把元素加到鏈表最后,把新元素的前驅指向之前的last元素,並把last元素指向新元素就ok。這也是一個O(1)的性能。
測試一下:
1 public static void main(String[] args) { 2 // TODO Auto-generated method stub 3 long begin = System.currentTimeMillis(); 4 5 // List<Object> list = new ArrayList<Object>(); 6 List<Object> list = new LinkedList<Object>(); 7 Object obj = new Object(); 8 for(int i=0; i<50000; i++){ 9 list.add(obj); 10 } 11 12 long end = System.currentTimeMillis(); 13 long time = end - begin; 14 System.out.println(time+""); 15 16 }
分別對ArrayList和LinkedList做末尾add操作,循環50000次,ArrayList耗時6ms,而LinkedList耗時8ms,這是由於LinkedList在add時候需要更多的對象創建和賦值操作。
在任意位置插入元素
ArrayList中的實現如下:
1 public void add(int index, E element) { 2 rangeCheckForAdd(index); 3 4 ensureCapacityInternal(size + 1); // Increments modCount!! 5 System.arraycopy(elementData, index, elementData, index + 1, 6 size - index); 7 elementData[index] = element; 8 size++; 9 }
這段代碼,首先先檢查數組容量,容量不夠先擴容,然后把index之后的數組往后挪一個,最后在index位置放上新元素。由於數組是一塊連續內存空間,所以在任意位置插入,都會導致這個其后數組后挪一位的情況,需要做一次數組復制操作,很明顯,如果有大量的隨機插入,那么這個數組復制操作開銷會很大,而且插入的越靠前,數組復制開銷越大。
LinkedList中的實現:
1 public void add(int index, E element) { 2 checkPositionIndex(index); 3 4 if (index == size) 5 linkLast(element); 6 else 7 linkBefore(element, node(index)); 8 } 9 10 void linkBefore(E e, Node<E> succ) { 11 // assert succ != null; 12 final Node<E> pred = succ.prev; 13 final Node<E> newNode = new Node<>(pred, e, succ); 14 succ.prev = newNode; 15 if (pred == null) 16 first = newNode; 17 else 18 pred.next = newNode; 19 size++; 20 modCount++; 21 }
這段代碼,取到原先index處節點的前驅,變成新節點的前驅 ,同時把原先index變成新節點的后驅,這樣就完成了新節點的插入。這個就是鏈表的優勢,不存在數據復制操作,性能和在最后插入是一樣的。
測試一種極端情況,每次都在最前端插入元素:
1 public static void main(String[] args) { 2 // TODO Auto-generated method stub 3 long begin = System.currentTimeMillis(); 4 5 // List<Object> list = new ArrayList<Object>(); 6 List<Object> list = new LinkedList<Object>(); 7 Object obj = new Object(); 8 for(int i=0; i<50000; i++){ 9 list.add(0,obj); 10 } 11 12 long end = System.currentTimeMillis(); 13 long time = end - begin; 14 System.out.println(time+""); 15 16 }
測試結果是:ArrayList耗時1400ms,而LinkedList只耗時12ms。可以看出,在隨機插入的時候,兩者的性能差異就很明顯了。
小結一下,從上面的源碼剖析和測試結果可以看出這三種List實現的一些典型適用場景,如果經常對數組做隨機插入操作,特別是插入的比較靠前,那么LinkedList的性能優勢就非常明顯,而如果都只是末尾插入,則ArrayList更占據優勢,如果需要線程安全,則非Vector莫屬。