1. 程式人生 > >List接口

List接口

nal 鏈表 tab 人員 完成 理論 設置 foreach 循環

  為方便開發人員進行程序開發,JDK提供了一組主要的數據結構實現,如List,Map,Set。網上有許多優秀的源碼解析,就不再做多余分析。本節主要討論List結構的使用方法和優化技巧。

  List是最重要的數據結構之一。常見又是最重要的三種List實現:ArrayList,Vector,LinkedList。三種List均來自AbstratList的實現,而AbstratList直接實現List接口,並擴展自AbstratCollection。

  其中ArrayList和Vector使用了shuzu實現。可以認為ArrayList或者Vector封裝了對內部數組的操作。比如向數組中添加、刪除、插入新的元素或者數組的擴展和重定義。對ArrayList或者Vector的操作等價對內部對象數據的操作。

  ArrayList和Vector幾乎使用了相同的算法,他們唯一區別可以認為是對多線程的支持。ArrayList沒有仁和一個方法做線程同步,因此不是線程安全。Vector中絕大部分方法都做了線程同步,
是一種線程安全的實現。因為ArrayList和Vector的性能相差無幾。從理論上將沒有實現線程同步的ArrayList要稍好於Vector。但實際表現並不明顯。

  LinkedList使用了循環雙向鏈表數據結構。與基於數組的List相比,這是兩種截然不同的實現技術,這也決定了他們將適用於完全不同的工作場景。以下是ArrayList和LinkedList的不同比較。

1.增加元素到列表尾端

  在ArrayList中增加元素到隊列尾端的代碼如下:

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 確保內部數組有足夠的空間
        elementData[size++] = e;  // 將元素加入到數組的末尾,完成添加
        return true;
    }

  ArrayList中的add()方法的性能取決於grow(),實現如下(基於JDK1.8)

private void grow(int minCapacity) {
        // overflow-conscious code
int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 擴容到原來的1.5倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 新數組長度小於最小時,為最小 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); //大於最大時為最大 // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }

  可以看到,只要ArrayList的當前容量足夠大,add()操作的效率是非常高的。

  當ArrayList對容量的需求超過當前數組的大小時,才需要進行擴容。擴容過程中會進行大量的數組復制操作。而數組復制時,最終將調用System.arraycopy()方法,因此add()操作的效率還是相當高的。

  LinkedList的add()操作實現如下,他也將任意元素增加到隊列的尾端:

public boolean add(E e) {
        linkLast(e);   // 將元素添加直尾端
        return true;
    }

  我們再深入linkLast()方法:

void linkLast(E e) {
   final Node<E> l = last;
   final Node<E> newNode = new Node<>(l, e, null);
   last = newNode;
   if (l == null)
      first = newNode;
   else
      l.next = newNode;
   size++;
   modCount++;
 }

  可見,LinkedList由於使用了鏈表的結構,因為不需要維護容量的大小。從這點上說,它比ArrayList有一定的性能優勢,然而每次元素增加都需要新建一個Node對象,並進行更多的賦值操作。在頻繁的系統調用中,對性能產生一定的影響。

  分別使用ArrayList和LinkedList運行一下代碼(-Xmx512M-Xms512M):

Object obj = new Object();
for(int i = 0; i < 500000; i++)
{ // 循環50萬次
    list.add(obj);
}

  使用-Xmx512M Xms512M的目的是屏蔽GC對程序執行 速度測量的幹擾(穩定的堆空間會減少GC次數,但也會增加每次GC時長)。ArrayList相對耗時16ms,而LinkedList相對耗時31ms。可見,不間斷地生成新的對象還是占用了一定的系統資源。

2.增加元素到列表任意位置

  除了提供了增加元素到List的尾端,List接口還提供了在任意位置插入元素的方法:

void add(int index, E element);

  由於實現上的不同,ArrayList和LinkedList在這個方法上存在一定的性能差異。由於ArrayList是基於數組實現,而數組是一塊連續的內存空間,如果在數組的任意位置插入元素,必然導致在該位置後的所有元素需要重新排列,因為效率相對比較低。代碼實現如下:

public void add(int index, E element) {
        rangeCheckForAdd(index); //  檢測是否存在越界風險

        ensureCapacityInternal(size + 1);  // 增加長度
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

  可以看到,每次插入操作,都會進行一次數組復制。而這個操作在增加元素到List尾端的時候是不存在的。大量的數組重組操作會導致系統性能底下。並且插入的元素在List中的位置越靠前,數組的重組開銷也越大。

  而LinkedList此時就顯示出了優勢:

public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

  可見,對LinkedList來說,在List尾端插入數據與在任意位置插入數據是一樣的。並不會因為插入的位置靠前而導致插入方法的性能降低。如果在系統應用中,List對象需要經常在任意位置插入元素,則可以考慮用LinkedList代替ArrayList以提高系統性能。

3.刪除任意位置元素

  對於元素刪除,List接口提供了在任意位置刪除元素的方法:

E remove(int index);

  對於ArrayList來說,remove()方法和ad()是雷同的,在任意位置移除元素後,都要進行數組的重組,ArrayList的實現如下:

public E remove(int index) {
        rangeCheck(index); // 是否存在越界風險

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0) // 將刪除位置後面的元素向前移動以為
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // 最後一個位置設置為null

        return oldValue; // 返回刪除元素
    }

  可以看到,在ArrayList的每一次有效元素刪除操作後,都要進行數組的重組。並且刪除的位置越靠前開銷越大。

public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {  // 要刪除的元素位於前半段
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {                            // 要刪除的元素位於後半段
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }            

  在LinkedList的實現中,首先要通過循環找到要刪除的位置處於List的前半段,則從前往後找;如其處於後半段,則從後往前找。因此,無論刪除靠前還是靠後的元素都非常高效。但要刪除較中間的元素幾乎要遍歷完半個List,在List擁有大量元素的情況下,效率很低。下面是測試代碼,所有數據均以一個擁有10萬個元素的List上進行,分別對ArrayList和LinkedList從LIst的頭部、中間和尾部進行刪除。

  從頭部刪除元素代碼:

while(list.size() > 0)
{
    list.remove(0);
}

  從List中間刪除元素代碼:

while(list.size() > 0)
{
    list.remove(list.size() >> 1);
}

  從List尾部刪除元素:

while(list.size() > 0)
{
    list.remove(list.size() - 1);
}

  從如下表中可以看到,對於ArrayList從尾部刪除元素是效率很高,符合上文分析,從頭部刪除元素時相當費時;而LinkedList從頭尾刪除元素時效率相差無幾,但是從List中間刪除元素時性能非常糟糕。

List類型/刪除位置 頭部 中間 尾部
ArrayList 6203  3125 16
LinkedList 15 8781 16

4.容量參數

  容量參數是ArrayList和Vector等基於數組的List的特有性能參數,它表示初始化的數組大小。由上文分析可知,當ArrayList所存儲的元素數量超過其已有大小時,它便會進行擴容,數組的擴容會導致整個數組進行一次內存復制。因此合理的數組大小有助於減少數組擴容的次數,從而提高系統性能。

  ArrayList默認大小為10,每次擴容後數組大小是原來大小的1.5倍。ArrayList也提供了一個可以指定初始數組大小的構造函數。此處未貼上源碼,如有需要可自行去看兩處構造函數。現以構造一個擁有100萬元素的List為例。當使用默認初始大小時,消耗的相對時間為125ms左右,當直接指定其數組大小為100萬時,構造相同的ArrayList僅相對耗時16ms。

  若指定JVM參數-Xms512M-Xms512M,再進行相同的測試。使用默認大小耗時47ms,指定ArrayList初始容量為100萬時,相對耗時16ms。可見通過提升堆內存大小,減少使用初始容量大小時的GC次數,ArrayList擴容時的數組復制,依然占用了較多的CPU時間。

  因為,能有效的評估ArrayList的數組大小初始值的情況下,指定容量大小對其性能有較大的提升。

5.遍歷列表

  遍歷列表操作也是常用的列表操作之一。在JDK1.5之後,至少有三種方式:ForEach操作、叠代器和for循環。以下代碼實現了三種遍歷方式:

        String tmp;
        long start = System.currentTimeMillis();
        // foreach遍歷
        for (String s : list)
        {
            tmp = s;
        }
        System.out.println("foreach spend:" + (System.currentTimeMillis() - start));
        
        // iterator遍歷
        start = System.currentTimeMillis();
        Iterator<String> it = list.iterator();
        while(it.hasNext())
        {
            tmp = it.next();
        }
        System.out.println("iterator spend:" + (System.currentTimeMillis() - start));
        
        // for循環
        start = System.currentTimeMillis();
        int size = list.size();
        for(int i = 0; i < size; i++)
        {
            tmp = list.get(i);
        }
        System.out.println("for spend:" + (System.currentTimeMillis() - start));            

  構造一個擁有100萬數據的ArrayList和等價的LinkedList,使用以上代碼測試,測試結果相對耗時如下:

List類型 ForEach操作 叠代器 for循環
ArrayList 63ms 47ms 31ms
LinkedList 63ms 47ms

  可以看到,最簡單的ForEach循環並沒有很好的性能表現,綜合性能不如普通的叠代器,而使用for循環通過隨機訪問遍歷列表時,ArrayList表現很好,但是LinkedList的表現卻無法讓人接受。因為LinkedList在進行隨機訪問時,總會進行一次列表的遍歷操作。

  對ArrayList這些基於數組的實現來說,隨機訪問速度是很快的,在遍歷這些List對象時,可以優先考慮隨機訪問。但對於基於LinkedList的鏈表實現,隨機訪問非常差,應盡量避免使用。

List接口