ArrayList和LinkedList使用不當,性能差距會如此之大!

 

前言

在面試的時候,常常會被問到幾個問題:html

ArrayList和LinkedList的區別,相信大部分朋友都能回答上:java

ArrayList是基於數組實現,LinkedList是基於鏈表實現git

當隨機訪問List時,ArrayList比LinkedList的效率更高,等等github

當被問到ArrayList和LinkedList的使用場景是什麼時,大部分朋友的答案多是:面試

ArrayList和LinkedList在新增、刪除元素時,LinkedList的效率要高於 ArrayList,而在遍歷的時候,ArrayList的效率要高於LinkedListubuntu

那這個回答是否準確呢?今天咱們就來研究研究!數組

咱們先來簡單介紹下ArrayList和LinkedList的原理實現!數據結構

源碼分析

ArrayList

實現類dom

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable 複製代碼

ArrayList實現了List接口,繼承了AbstractList抽象類,底層是數組實現的,而且實現了自增擴容數組大小。函數

ArrayList還實現了Cloneable接口和Serializable接口,因此他能夠實現克隆和序列化。

ArrayList還實現了RandomAccess接口,這個接口是一個標誌接口,他標誌着「只要實現該接口的List類,都能實現快速隨機訪問」。

基本屬性

ArrayList屬性主要由數組長度size、對象數組elementData、初始化容量default_capacity等組成, 其中初始化容量默認大小爲10。

//默認初始化容量
private static final int DEFAULT_CAPACITY = 10;
//對象數組
transient Object[] elementData; 
//數組長度
private int size;
複製代碼

從ArrayList屬性來看,elementData被關鍵字transient修飾了,transient關鍵字修飾該字段則表示該屬性不會被序列化。

但ArrayList實際上是實現了序列化接口,這是爲何呢?

因爲ArrayList的數組是基於動態擴增的,因此並非全部被分配的內存空間都存儲了數據。

若是採用外部序列化法實現數組的序列化,會序列化整個數組,ArrayList爲了不這些沒有存儲數據的內存空間被序列化,內部提供了兩個私有方法writeObject以及readObject來自我完成序列化與反序列化,從而在序列化與反序列化數組時節省了空間和時間。

所以使用transient修飾數組,是防止對象數組被其餘外部方法序列化。

ArrayList自定義序列化方法以下:

初始化

有三種初始化辦法:無參數直接初始化、指定大小初始化、指定初始數據初始化,源碼以下:

當ArrayList新增元素時,若是所存儲的元素已經超過其已有大小,它會計算元素大小後再進行動態擴容,數組的擴容會致使整個數組進行一次內存複製。

所以,咱們在初始化ArrayList時,能夠經過第一個構造函數合理指定數組初始大小,這樣有助於減小數組的擴容次數,從而提升系統性能。

注意點:

ArrayList 無參構造器初始化時,默認大小是空數組,並非你們常說的 10,10 是在第一次 add 的時候擴容的數組值。

新增元素

ArrayList新增元素的方法有兩種,一種是直接將元素加到數組的末尾,另一種是添加元素到任意位置。

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

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

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
複製代碼

兩個方法的相同之處是在添加元素以前,都會先確認容量大小,若是容量夠大,就不用進行擴容;若是容量不夠大,就會按照原來數組的1.5倍大小進行擴容,在擴容以後須要將數組複製到新分配的內存地址。

下面是具體的源碼:

這兩個方法也有不一樣之處,添加元素到任意位置,會致使在該位置後的全部元素都須要從新排列,而將元素添加到數組的末尾,在沒有發生擴容的前提下,是不會有元素複製排序過程的。

因此ArrayList在大量新增元素的場景下效率不必定就很慢的

若是咱們在初始化時就比較清楚存儲數據的大小,就能夠在ArrayList初始化時指定數組容量大小,而且在添加元素時,只在數組末尾添加元素,那麼ArrayList在大量新增元素的場景下,性能並不會變差,反而比其餘List集合的性能要好。

刪除元素

ArrayList 刪除元素有不少種方式,好比根據數組索引刪除、根據值刪除或批量刪除等等,原理和思路都差很少。

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

咱們選取根據值刪除方式來進行源碼說明:

遍歷元素

因爲ArrayList是基於數組實現的,因此在獲取元素的時候是很是快捷的。

public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    E elementData(int index) {
        return (E) elementData[index];
    }
複製代碼

LinkedList

LinkedList是基於雙向鏈表數據結構實現的。

這個雙向鏈表結構,鏈表中的每一個節點均可以向前或者向後追溯,有幾個概念以下:

  • 鏈表每一個節點咱們叫作 Node,Node 有 prev 屬性,表明前一個節點的位置,next 屬性,表明後一個節點的位置;
  • first 是雙向鏈表的頭節點,它的前一個節點是 null。
  • last 是雙向鏈表的尾節點,它的後一個節點是 null;
  • 當鏈表中沒有數據時,first 和 last 是同一個節點,先後指向都是 null;
  • 由於是個雙向鏈表,只要機器內存足夠強大,是沒有大小限制的。

Node結構中包含了3個部分:元素內容item、前指針prev以及後指針next,代碼以下。

private static class Node<E> {
    E item;// 節點值
    Node<E> next; // 指向的下一個節點
    Node<E> prev; // 指向的前一個節點

    // 初始化參數順序分別是:前一個節點、自己節點值、後一個節點
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
複製代碼

LinkedList就是由Node結構對象鏈接而成的一個雙向鏈表。

實現類

LinkedList類實現了List接口、Deque接口,同時繼承了AbstractSequentialList抽象類,LinkedList既實現了List類型又有Queue類型的特色;LinkedList也實現了Cloneable和Serializable接口,同ArrayList同樣,能夠實現克隆和序列化。

因爲LinkedList存儲數據的內存地址是不連續的,而是經過指針來定位不連續地址,所以,LinkedList不支持隨機快速訪問,LinkedList也就不能實現RandomAccess接口。

public class LinkedList extends AbstractSequentialList implements List, Deque, Cloneable, java.io.Serializable 複製代碼

基本屬性

transient int size = 0;
transient Node first;
transient Node last;
複製代碼

咱們能夠看到這三個屬性都被transient修飾了,緣由很簡單,咱們在序列化的時候不會只對頭尾進行序列化,因此LinkedList也是自行實現readObject和writeObject進行序列化與反序列化。

下面是LinkedList自定義序列化的方法。

節點查詢

鏈表查詢某一個節點是比較慢的,須要挨個循環查找才行,咱們看看 LinkedList 的源碼是如何尋找節點的:

LinkedList 並無採用從頭循環到尾的作法,而是採起了簡單二分法,首先看看 index 是在鏈表的前半部分,仍是後半部分。

若是是前半部分,就從頭開始尋找,反之亦然。經過這種方式,使循環的次數至少下降了一半,提升了查找的性能。

新增元素

LinkedList添加元素的實現很簡潔,但添加的方式卻有不少種。

默認的add (Ee)方法是將添加的元素加到隊尾,首先是將last元素置換到臨時變量中,生成一個新的Node節點對象,而後將last引用指向新節點對象,以前的last對象的前指針指向新節點對象。

LinkedList也有添加元素到任意位置的方法,若是咱們是將元素添加到任意兩個元素的中間位置,添加元素操做只會改變先後元素的先後指針,指針將會指向添加的新元素,因此相比ArrayList的添加操做來講,LinkedList的性能優點明顯。

刪除元素

在LinkedList刪除元素的操做中,咱們首先要經過循環找到要刪除的元素,若是要刪除的位置處於List的前半段,就從前日後找;若其位置處於後半段,就從後往前找。

這樣作的話,不管要刪除較爲靠前或較爲靠後的元素都是很是高效的,但若是List擁有大量元素,移除的元素又在List的中間段,那效率相對來講會很低。

遍歷元素

LinkedList的獲取元素操做實現跟LinkedList的刪除元素操做基本相似,經過分先後半段來循環查找到對應的元素,可是經過這種方式來查詢元素是很是低效的,特別是在for循環遍歷的狀況下,每一次循環都會去遍歷半個List。

因此在LinkedList循環遍歷時,咱們可使用iterator方式迭代循環,直接拿到咱們的元素,而不須要經過循環查找List。

分析測試

新增元素操做性能測試

測試用例源代碼:

測試結果:

操做 花費時間
從集合頭部位置添加元素(ArrayList) 550
從集合頭部位置添加元素(LinkedList) 34
從集合中間位置位置添加元素(ArrayList) 32
從集合中間位置位置添加元素(LinkedList) 58746
從集合尾部位置添加元素(ArrayList) 29
從集合尾部位置添加元素(LinkedList) 31

經過這組測試,咱們能夠知道LinkedList添加元素的效率未必要高於ArrayList。

從集合頭部位置添加元素

因爲ArrayList是數組實現的,在添加元素到數組頭部的時候,須要對頭部之後的數據進行復制重排,因此效率很低;

LinkedList是基於鏈表實現,在添加元素的時候,首先會經過循環查找到添加元素的位置,若是要添加的位置處於List的前半段,就從前日後找;若其位置處於後半段,就從後往前找,所以LinkedList添加元素到頭部是很是高效的。

從集合中間位置位置添加元素

ArrayList在添加元素到數組中間時,一樣有部分數據須要複製重排,效率也不是很高;

LinkedList將元素添加到中間位置,是添加元素最低效率的,由於靠近中間位置,在添加元素以前的循環查找是遍歷元素最多的操做。

從集合尾部位置添加元素

而在添加元素到尾部的操做中,在沒有擴容的狀況下,ArrayList的效率要高於LinkedList。

這是由於ArrayList在添加元素到尾部的時候,不須要複製重排數據,效率很是高。

LinkedList雖然也不用循環查找元素,但LinkedList中多了new對象以及變換指針指向對象的過程,因此效率要低於ArrayList。

注意:這是排除動態擴容數組容量的狀況下進行的測試,若是有動態擴容的狀況,ArrayList的效率也會下降。

刪除元素操做性能測試

ArrayList和LinkedList刪除元素操做測試的結果和添加元素操做測試的結果很接近!

結論: 若是須要在List的頭部進行大量的插入、刪除操做,那麼直接選擇LinkedList。不然,ArrayList便可。

遍歷元素操做性能測試

測試用例源代碼:

測試結果:

操做 花費時間
for循環(ArrayList) 3
for循環(LinkedList) 17557
迭代器循環(ArrayList) 4
迭代器循環(LinkedList) 4

咱們能夠看到,LinkedList的for循環性能是最差的,而ArrayList的for循環性能是最好的。

這是由於LinkedList基於鏈表實現的,在使用for循環的時候,每一次for循環都會去遍歷半個List,因此嚴重影響了遍歷的效率;ArrayList則是基於數組實現的,而且實現了RandomAccess接口標誌,意味着ArrayList能夠實現快速隨機訪問,因此for循環效率很是高。

LinkedList的迭代循環遍歷和ArrayList的迭代循環遍歷性能至關,也不會太差,因此在遍歷LinkedList時,咱們要切忌使用for循環遍歷。

最後

創做不易,若是對你們有所幫助,但願你們點贊支持,有什麼問題也能夠在評論區裏討論😄~

若是你以爲這篇文章對你有點用的話,麻煩請給咱們的開源項目點點star: http://github.crmeb.net/u/defu 不勝感激 !
來自 「開源世界 」 ,連接: https://ym.baisou.ltd/post/778.html
相關文章
相關標籤/搜索