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

前言

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

對於這兩個集合類,相信你們都不陌生,ArrayList能夠說是平常開發中用的最多的工具類了,也是面試中幾乎必問的,LinkedList可能用的少點,但大多數的面試也會有所涉及,尤爲是關於這二者的比較能夠說是屢見不鮮,因此,不管從使用上仍是在面試的準備上,對於這兩個類的知識點咱們都要有足夠的瞭解。node

ArrayList

ArrayList是List接口的一個實現類,底層是基於數組實現的存儲結構,能夠用於裝載數據,數據都是存放到一個數組變量中,web

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的最重要的緣由。性能

插入數據

可是,數組的容量是肯定的啊,若是要存儲的數據大小超過了數組大小,那不就有數組越界的問題?flex

關於這點,咱們不用擔憂,ArrayList幫咱們作了動態擴容的處理,若是發現新增數據後,List的大小已經超過數組的容量的話,就會新增一個爲原來1.5倍容量的新數組,而後把原數組的數據原封不動的複製到新數組中,再把新數組賦值給原來的數組對象就完成了。優化

ArrayList擴容

擴容以後,數組的容量足夠了,就能夠正常新增數據了。

除此以外,ArrayList提供支持指定index新增的方法,就是能夠把數據插入到設定的索引下標,好比說我想把元素4插入到3後面的位置,也就是如今5所在的地方,

指定index位置

插入數據的時候,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位置先後的兩個節點便可,好比咱們要在李白和韓信之間插入孫悟空的節點,只須要像這樣處理下節點之間的指向地址:

LinkedList插入數據

刪除數據也是一樣原理,只須要改變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這部分多餘的空間不會被序列化。

怎麼樣,這樣的回答是否是更加的說服力,不只更加全面,還可能會給面試官留下好印象,讓他以爲你是個有本身思考的求職者,說不定當場就讓你面試經過了呢。就衝這點,大家是否是應該給我來個點個贊呢,哈哈。


做者:鄙人薛某,一個不拘於技術的互聯網人,歡迎關注個人公衆號,這裏不只有技術乾貨,還有吹水~~~

相關文章
相關標籤/搜索