讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議68:頻繁插入和刪除時使用LinkedList >

建議68:頻繁插入和刪除時使用LinkedList

上一個建議介紹了列表的遍歷方式,也就是「讀」操作,本建議將介紹列表的「寫」操作:即插入、刪除、修改動作。

(1)插入元素

列表中我們使用最多的是ArrayList,下面來看看它的插入(add方法)算法,源代碼如下:


public void add(int index, E element){

/*檢查下標是否越界,代碼不再拷貝*/

//若需要擴容,則增大底層數組的長度

ensureCapacity(size+1);

//給index下標之後的元素(包括當前元素)的下標加1,空出index位置

System.arraycopy(elementData, index, elementData, index+1,size-index);

//賦值index位置元素

elementData[index]=element;

//列表長度+1

size++;

}


注意看arraycopy方法,只要是插入一個元素,其後的元素就會向後移動一位,雖然arraycopy是一個本地方法,效率非常高,但頻繁的插入,每次後面的元素都要拷貝一遍,效率就變低了,特別是在頭位置插入元素時。現在的問題是,開發中確實會遇到要插入元素的情況,那有什麼更好的方法解決此效率問題嗎?

有,使用LinkedList類即可。我們知道LinkedList是一個雙向鏈表,它的插入只是修改相鄰元素的next和previous引用,其插入算法(add方法)如下:


public void add(int index, E element){

addBefore(element,(index==size?header:entry(index)));

}


這裡調用了私有addBefore方法,該方法實現了在一個元素之前插於元素的算法,代碼如下:


private Entry<E>addBefore(E e, Entry<E>entry){

//組裝一個新節點,previous指向原節點的前節點,next指向原節點

Entry<E>newEntry=new Entry<E>(e, entry, entry.previous);

//前節點的next指向自己

newEntry.previous.next=newEntry;

//後節點的previous指向自己

newEntry.next.previous=newEntry;

//長度+1

size++;

//修改計數器+1

modCount++;

return newEntry;

}


這是一個典型的雙向鏈表插入算法,把自己插入到鏈表,然後再把前節點的next和後節點的previous指向自己。想想看,這樣一個插入元素(也就是Entry對像)的過程中,沒有任何元素會有拷貝過程,只是引用地址改變了,那效率當然就高了。

經過實際測試得知,LinkedList的插入效率比ArrayList快50倍以上。

(2)刪除元素

插入瞭解清楚了,我們再來看刪除動作。ArrayList提供了刪除指定位置上的元素、刪除指定值元素、刪除一個下標範圍內的元素集等刪除動作,三者的實現原理基本相似,都是找到索引位置,然後刪除。我們以最常用的刪除指定下標的方法(remove方法)為例來看看刪除動作的性能到底如何,源碼如下:


public E remove(int index){

//下標校驗

RangeCheck(index);

//修改計數器+1

modCount++;

//記錄要刪除的元素值

E oldValue=(E)elementData[index];

//有多少個元素向前移動

int numMoved=size-index-1;

if(numMoved>0)

//index後的元素向前移動一位

System.arraycopy(elementData, index+1,elementData, index, numMoved);

//列表長度減1,並且最後一位設為null

elementData[--size]=null;

//返回刪除的值

return oldValue;

}


注意看,index位置後的元素都向前移動了一位,最後一個位置空出來了,這又是一次數組拷貝,和插入一樣,如果數據量大,刪除動作必然會暴露出性能和效率方面的問題。ArrayList其他的兩個刪除方法與此相似,不再贅述。

我們再來看看LinkedList的刪除動作。LinkedList提供了非常多的刪除操作,比如刪除指定位置元素、刪除頭元素等,與之相關的poll方法也會執行刪除動作,下面來看最基本的刪除指定位置元素的方法remove,源代碼如下:


private E remove(Entry<E>e){

//取得原始值

E result=e.element;

//前節點next指向當前節點的next

e.previous.next=e.next;

//後節點的previouse指向當前節點的previous

e.next.previous=e.previous;

//置空當前節點的next和previous

e.next=e.previous=null;

//當前元素置空

e.element=null;

//列表長度減1

size--;

//修改計數器+1

modCount++;

return result;

}


這也是雙向鏈表的標準刪除算法,沒有任何耗時的操作,全部是引用指針的變更,效率自然高了。

在實際測試中得知,處理大批量的刪除動作,LinkedList比ArrayList快40倍以上。

(3)修改元素

寫操作還有一個動作:修改元素值,在這一點上LinkedList輸給了ArrayList,這是因為LinkedList是順序存取的,因此定位元素必然是一個遍歷過程,效率大打折扣,我們來看set方法的代碼:


public E set(int index, E element){

//定位節點

Entry<E>e=entry(index);

E oldVal=e.element;

//節點的元素替換

e.element=element;

return oldVal;

}


看似很簡潔,但是這裡使用了entry方法定位元素,在上一個建議中我們已經說明了LinkedList這種順序存取列表的元素定位方式會折半遍歷,這是一個極耗時的操作。而ArrayList的修改動作則是數組元素的直接替換,簡單高效。

在修改動作上,LinkedList比ArrayList慢很多,特別是要進行大量的修改時,兩者完全不在一個數量級上。

上面通過分析源碼完成了LinkedList與ArrayList之間的PK,其中LinkedList勝兩局:刪除和插入效率高;ArrayList勝一局:修改元素效率高。總體上來說,在「寫」方面,LinkedList佔優勢,而且在實際使用中,修改是一個比較少的動作。因此,如果有大量的寫操作(更多的是插入和刪除動作),推薦使用LinkedList。不過何為少量,何為大量呢?

這就要依賴諸位正在開發的系統了,一個實時交易的系統,即使寫作操再少,使用LinkedList也比ArrayList合適,因為此類系統是爭分奪秒的,多N個毫秒可能就會造成交易數據不準確;而對於一個批量系統來說,幾十毫秒、幾百毫秒,甚至是幾千毫秒的差別意義都不大,這時是使用LinkedList還是ArrayList就看個人愛好了,當然,如果系統已經處於性能臨界點了那就必須使用LinkedList。

且慢,「寫」操作還有一個增加(add方法)操作,為什麼這裡沒有PK呢?那是因為兩者在增加元素時性能上基本沒有什麼差別,區別只是在增加時LinkedList生成了一個Entry元素,其previous指向倒數第二個Entry, next置空;而ArrayList則是把元素追加到了數組中而已,兩者的性能差別非常微小,不再討論。