說說JDK中的List-ArrayList、Vector、LinkedList


為方便開發人員,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莫屬。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM