前言
相信在面試的時候可能都會碰到關於ArrayList和Linkedlist相關的面試題。趁此機會也記錄一下。
ArrayList
ArrayList是List接口的一個實現類,底層是Object數組,數據放在一個變量里面:
transient Object[] elementData;
而這個elementData數組使用了一個transient關鍵字修飾,transient表示的意思是:序列化對象的時候,如果在屬性前面加上了該關鍵字,那么在序列化是就不會將該屬性序列化。
ArrayList初始化時默認數組長度為10,隨着我們不斷的往list中插入數據,當list大小超過容量時就會進行擴容,每次擴容的大小為之前的1.5倍。可以這么理解,當list已經足夠大時,如果剛好插入的數據滿足擴容的條件,那么擴容之后的list容量為之前的1.5,於是這個空間已經擴展的很大了。
由於ArrayList底層是數組,數組我們都知道可以根據索引來定位,相應ArrayList查找性能很快。但是數組我們又知道是一連串的空間組成,於是當我們在中間插入或者刪除一個元素時,插入或刪除位置之后的元素是不是都要向后移動或向前移動一位,所以插入或者刪除都比較消耗性能。最后我們又知道,既然你是在中間插入或刪除時后面元素需要移動,那我在最后插入元素是不是很快呢?是的,最后插入由於不需要移動元素,插入的效率很高。總結:ArrayList底層是數組,可以根據索引查找元素,查找效率很高,在數組中間插入或刪除元素由於需要移動數據導致效率很低,但是在最后插入元素效率很高。
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; } }
因為有保存前后節點的地址,LinkedList增刪數據的時候不需要像ArrayList那樣移動整片的數據,只需要通過引用指定index位置前后的兩個節點即可。刪除數據只需要改變index位置前后節點的指向地址即可。雖然增刪數據很快,但是查詢數據效率就比較低,查詢數據時,首先會計算下鏈表總長度的一半,判斷對應索引是在該值的左邊還是右邊,然后決定從頭節點還是尾節點開始遍歷。所以,Linkedlist增刪數據較快,查詢數據較慢,並且對內存占用空間也是很大的,因為每個node都維護這前后指向地址的節點。
誰更占空間?
表面上看,肯定是Linkedlist更占用空間,但是ArrayList擴容的時候會把之前的容量擴充為1.5倍。你可以試想下,我往ArrayList只想加一個元素,但剛好滿足擴容條件,那是不是剩下的空間就浪費了呢。況且ArrayList本身就很大了,擴容1.5倍。。。
所以,如果數據量很大又在實時添加數據的情況下,ArrayList占用的空間不一定會比LinkedList空間小,這樣的回答就顯得謹慎些了,聽上去也更加讓人容易認同,但你以為這樣回答就完美了嗎?還記得我前面說的那個transient變量嗎?它的作用已經說了,不想序列化的對象就可以用它來修飾,用transient修飾elementData意味着我不希望elementData數組被序列化。
為什么要這么做呢?這是因為序列化ArrayList的時候,ArrayList里面的elementData,也就是數組未必是滿的,比方說elementData有10的大小,但是我只用了其中的3個,那么是否有必要序列化整個elementData呢? 顯然沒有這個必要,因此ArrayList中重寫了writeObject方法。
每次序列化的時候調用這個方法,先調用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData這個數組對象不去序列化它,而是遍歷elementData,只序列化數組里面有數據的元素,這樣一來,就可以加快序列化的速度,還能夠減少空間的開銷。
最后,一般情況下,LinkedList的占用空間更大,因為每個節點要維護指向前后地址的兩個節點,但也不是絕對,如果剛好數據量超過ArrayList默認的臨時值時,ArrayList占用的空間也是不小的,因為擴容的原因會浪費將近原來數組一半的容量,不過,因為ArrayList的數組變量是用transient關鍵字修飾的,如果集合本身需要做序列化操作的話,ArrayList這部分多余的空間不會被序列化。