1. 程式人生 > >當面試官問我ArrayList和LinkedList哪個更佔空間時,我這麼答讓他眼前一亮

當面試官問我ArrayList和LinkedList哪個更佔空間時,我這麼答讓他眼前一亮

前言

今天介紹一下Java的兩個集合類,ArrayList和LinkedList,這兩個集合的知識點幾乎可以說面試必問的。

對於這兩個集合類,相信大家都不陌生,ArrayList可以說是日常開發中用的最多的工具類了,也是面試中幾乎必問的,LinkedList可能用的少點,但大多數的面試也會有所涉及,尤其是關於這兩者的比較可以說是家常便飯,所以,無論從使用上還是在面試的準備上,對於這兩個類的知識點我們都要有足夠的瞭解。

ArrayList

ArrayList是List介面的一個實現類,底層是基於陣列實現的儲存結構,可以用於裝載資料,資料都是存放到一個數組變數中,

transient Object[] elementData;

transient是一個關鍵字,它的作用可以總結為一句話:將不需要序列化的屬性前新增關鍵字transient,序列化物件的時候,這個屬性就不會被序列化。 你可能會覺得奇怪,ArrayList可以被序列化的啊,原始碼可是實現了java.io.Serializable介面啊,為什麼陣列變數還要用transient定義呢?

別急,關於這個問題,我們後面會討論到,不賣個關子,你們怎麼會看到最後,然後給我點在看呢?

當我們新建一個例項時,ArrayList會預設幫我們初始化陣列的大小為10

/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

但請注意,這個只是陣列的容量大小,並不是List真正的大小,List的大小應該由儲存資料的數量決定,在原始碼中,獲取真實的容量其實是用一個變數size來表示,

private int size;

在原始碼中,資料預設是從陣列的第一個索引開始儲存的,當我們新增資料時,ArrayList會把資料填充到上一個索引的後面去,所以,ArrayList的資料都是有序排列的。而且,由於ArrayList本身是基於陣列儲存,所以查詢的時候只需要根據索引下標就可以找到對於的元素,查詢效能非常的高,這也是我們非常青睞ArrayList的最重要的原因。

但是,陣列的容量是確定的啊,如果要儲存的資料大小超過了陣列大小,那不就有陣列越界的問題?

關於這點,我們不用擔心,ArrayList幫我們做了動態擴容的處理,如果發現新增資料後,List的大小已經超過陣列的容量的話,就會新增一個為原來1.5倍容量的新陣列,然後把原陣列的資料原封不動的複製到新陣列中,再把新陣列賦值給原來的陣列物件就完成了。

擴容之後,陣列的容量足夠了,就可以正常新增資料了。

除此之外,ArrayList提供支援指定index新增的方法,就是可以把資料插入到設定的索引下標,比如說我想把元素4插入到3後面的位置,也就是現在5所在的地方,

插入資料的時候,ArrayList的操作是先把3後面的陣列全部複製一遍,然後將這部分資料往後移動一位,其實就是逐個賦值給後移一位的索引位置,然後3後面就可以空出一個位置,把4放入就完成了插入資料的操作了

刪除的時候也是一樣,指定index,然後把後面的資料拷貝一份,並且向前移動,這樣原來index位置的資料就刪除了。

到這裡我們也不難發現,這種基於陣列的查詢雖然高效,但增刪資料的時候卻很耗效能,因為每增刪一個元素就要移動對應index後面的所有元素,資料量少點還無所謂,但如果儲存上千上萬的資料就很吃力了,所以,如果是頻繁增刪的情況,不建議用ArrayList。

既然ArrayList不建議用的話,這種情況下有沒有其他的集合可用呢?

當然有啊,像我這樣的暖男肯定是第一時間告訴你們的,這就引出了我們下面要說的LinkedList。

LinkedList

LinkedList 是基於雙向連結串列實現的,不需要指定初始容量,連結串列中任何一個儲存單元都可以通過向前或者向後的指標獲取到前面或者後面的儲存單元。在 LinkedList 的原始碼中,其儲存單元用一個Node類表示:

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;
    }
}

Node中包含了三個成員,分別是儲存資料的item,指向前一個儲存單元的點 prev 和指向後一個儲存單元的節點 next ,通過這兩個節點就可以關聯前後的節點,組裝成為連結串列的結構,

因為有儲存前後節點的地址,LinkedList增刪資料的時候不需要像ArrayList那樣移動整片的資料,只需要通過引用指定index位置前後的兩個節點即可,比如我們要在李白和韓信之間插入孫悟空的節點,只需要像這樣處理下節點之間的指向地址:

刪除資料也是同樣原理,只需要改變index位置前後兩個節點的指向地址即可。

這樣的連結串列結構使得LinkedList能非常高效的增刪資料,在頻繁增刪的情景下能很好的使用,但不足之處也是有的。

雖然增刪資料很快,但查詢就不怎麼樣了,LinkedList是基於雙向連結串列儲存的,當查詢對應index位置的資料時,會先計算連結串列總長度一半的值,判讀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最無奈的地方,魚和熊掌不可兼得,我們既想查的快,又想增刪快,這樣的好事怎麼可能都讓我們遇到呢?所以,一般建議LinkedList使用於增刪多,查詢少的情景。

除此之外,LinkedList對記憶體的佔用也是比較大的,畢竟每個Node都維護著前後指向地址的節點,資料量大的話會佔用不少記憶體空間。

兩者哪個更佔空間?

講到這,你是不是對標題的那個問題成竹在胸了?

下次有面試官問你,ArrayList和LinkedList哪個更佔空間時,你就可以信誓旦旦的說,LinkedList更佔空間,我看了薛大佬的文章,肯定不會錯。說完你就可以安心坐著,等待面試官露出滿意的笑容,告訴你通過面試的訊息,成功拿下offer指日可待。

如果你真的這麼答的話,我也相信面試官一定會被你的回答所征服,他聽完一定會點點頭,嘴角開始上揚,然後笑容滿面的告訴你,

感謝你今天過來面試,你可以回去等通知了。。。。

哈哈,開個玩笑,不湊多點字可不是我的風格。

言歸正傳,表面上看,LinkedList的Node儲存結構似乎更佔空間,但別忘了前面介紹ArrayList擴容的時候,它會預設把陣列的容量擴大到原來的1.5倍的,如果你只新增一個元素的話,那麼會有將近原來一半大小的陣列空間被浪費了,如果原先陣列很大的話,那麼這部分空間的浪費也是不少的,

所以,如果資料量很大又在實時新增資料的情況下,ArrayList佔用的空間不一定會比LinkedList空間小,這樣的回答就顯得謹慎些了,聽上去也更加讓人容易認同,但你以為這樣回答就完美了嗎?非也

還記得我前面說的那個transient變數嗎?它的作用已經說了,不想序列化的物件就可以用它來修飾,用transient修飾elementData意味著我不希望elementData陣列被序列化。為什麼要這麼做呢?

這是因為序列化ArrayList的時候,ArrayList裡面的elementData,也就是陣列未必是滿的,比方說elementData有10的大小,但是我只用了其中的3個,那麼是否有必要序列化整個elementData呢? 顯然沒有這個必要,因此ArrayList中重寫了writeObject方法:

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

每次序列化的時候呼叫這個方法,先呼叫defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData這個陣列物件不去序列化它,而是遍歷elementData,只序列化數組裡面有資料的元素,這樣一來,就可以加快序列化的速度,還能夠減少空間的開銷。

加上這個知識點後,我們對上面那個問題就可以有更加全面的回答了,如果你下次也遇到這個問題的話,你可以參考一下我的說法:

一般情況下,LinkedList的佔用空間更大,因為每個節點要維護指向前後地址的兩個節點,但也不是絕對,如果剛好資料量超過ArrayList預設的臨時值時,ArrayList佔用的空間也是不小的,因為擴容的原因會浪費將近原來陣列一半的容量,不過,因為ArrayList的陣列變數是用transient關鍵字修飾的,如果集合本身需要做序列化操作的話,ArrayList這部分多餘的空間不會被序列化。

怎麼樣,這樣的回答是不是更加的說服力,不僅更加全面,還可能會給面試官留下好印象,讓他覺得你是個有自己思考的求職者,說不定當場就讓你面試通過了呢。就衝這點,你們是不是應該給我來個點個贊呢,哈哈。


作者:鄙人薛某,一個不拘於技術的網際網路人,歡迎關注我的公眾號,這裡不僅有技術乾貨,還有吹水~~~