[改善Java代碼]頻繁插入和刪除時使用LinkedList


 一、分析

前面有文章分析了列表的表里方式,也就是“讀”的操作。本文將介紹表的“寫”操作:即插入、刪除、修改動作。

二、場景

 1.插入元素

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

 1     public void add(int index,E element){
 2         /*檢查下標是否越界,代碼不在拷貝*/
 3         //若需要擴容,則增大底層數組的長度
 4         ensureCapacity(size + 1);
 5         //給index下標之后的元素(包括當前元素)的下標加1,空出index位置(將elementData從index起始,復制到index+1的職位
 6         System.arraycopy(elementData,index,elementData,index + 1,size - index);
 7         //賦值index位置元素
 8         elementData[index] = element;
 9         //列表的長度+1
10         size++;
11         }
12       }    

注意看arraycopy方法,只要是插入一個元素,其后的元素就會向后移動一位,雖然arraycopy是一個本地方法,效率非常高,但頻繁的插入,每次后面的元素都需要拷貝一遍,效率變低了,特別是在頭位置插入元素時。

現在的問題是,開發中確實會遇到要插入元素的情況,那有什么更好的方法來解決此效率問題嗎?

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

1 public void add(int index,E element){
2   addBefore(element,(index==size?header:entry(index)));
3 }

 

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

 1 private Entry addBefore(E e,Entry entry){
 2   //組裝一個新的節點,previous指向原節點的前節點,next指向原節點
 3   Entry newEntry = new Entry(e,entry,entry.previous);
 4   //前節點的next指向自己
 5   newEntry.previous.next = newEntry;
 6   //后節點的previous指向自己
 7   newEntry.next.previous = newEntry;
 8   //長度+1
 9   size++;
10   //修改計數器+1
11   modCount ++;
12   return newEntry;
13 }

 

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

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

(2)刪除元素

ArrayList刪除指定位置上的元素、刪除指定值元素,刪除一個下標范圍內的元素集等刪除動作,三者的實現原理基本相似,都是找到索引位置,然后刪除。我偶們常用的刪除下標的方法(remove方法)為例來看看刪除動作的性能到底如何,源碼如下:

 1 public E remove(int index){
 2   //下標校驗
 3   RangeCheck(index);
 4   //修改計數器+1
 5   modCount++;
 6   //記錄要刪除的元素
 7   E oldValue = (E)elementData(index);
 8   //有多少個元素向前移動
 9   int numMoved = size - index - 1;
10   if(numMoved > 0)
11   //index后的元素向前移動一位
12   System.arraycopy(elementData,index + 1,elementData,index,numMoved);
13   //列表長度減1,並且最后一位設為null
14   elementData[--size] = null;
15   //返回刪除的值
16   return oldValue;
17 }

注意看,index位置后的元素都向前移動了一位,最后一個位置空出來了,這又是一次數組拷貝,和插入一樣,如果數據量大,刪除動作必然會暴露出性能和效率方面的問題。

我們再來看看LinkedList的刪除動作,比如刪除指定位置元素,刪除頭元素等。我們看看最基本的刪除指定位置元素的方法remove,源代碼如下:

 1 private E remove(Entry e){
 2   //取得原始值
 3   E result = e.element;
 4   //前節點next指向當前節點的next
 5   e.previous.next = e.next;
 6   //后節點的previouse指向當前節點的previous
 7   e.next.previous = e.previous;
 8   //置空當前節點的next和previous
 9   e.next = e.previous= null;
10   //當前元素置空
11   e.element = null;
12   //列表長度減1
13   size --;
14   //修改計數器+1
15   modCount++;
16   return result;
17 }

 

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

實際測試可知,處理大批量的刪除操作,LinkedList比ArrayList塊40倍以上。

(3)修改元素

寫操作還有一個動作:修改元素,在這點上LinkedList輸給了ArrayList,這是因為,LinkedList是順序存取的,因此定位元素必然是一個遍歷的過程,效率大大折扣。

我們來開set方法的代碼:

1 public E set(int index,E element){
2   //定位節點
3   Entry e = entry(index);
4   E oldVal = e.element;
5   //節點元素替換
6   e.element = element;
7   return oldVal;
8 }

 

看似很簡潔,這里使用了entry方法定位元素。而LinkedList這種順序取列表的元素定位方式會折半遍歷,這是一個極其耗時的操作。而ArrayList的修改動作則是數組元素的直接替換,簡單高效。

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

上面通過源代碼分析完成了ArrayList和LinkedList之間的PK,其中LinkedList勝兩局:刪除和插入效率更高;ArrayList勝一局,修改效率更高.

總體上來說,在"寫"方面LinkedList占優勢,而且在實際使用中,修改上一個比較少的動作.因此,如果有大量寫的操作(更多的是插入和刪除操作),推薦使用LinkedList.

 

不過何為少量,何為大量..

這就需要依賴於開發的系統了..一個實時的交易系統,即使寫的操作再少,使用LinkedList也比ArrayList合適.因為此類系統是爭分奪秒的,多N個毫秒可能就會造成交易數據的不准確.

而對於一個批量系統來說,幾十毫秒,幾百毫秒,甚至是幾千毫秒的差別和意義都不大,這時候使用LinkedList和ArrayList就看個人的愛好了,當然,如果系統已經處於性能臨界點了那就必須使用LinkedList.

 


免責聲明!

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



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