在一開始基礎面的時候,不少面試官可能會問List集合一些基礎知識,好比:java
ArrayList
默認大小是多少,是如何擴容的?ArrayList
和LinkedList
的底層數據結構是什麼?ArrayList
和LinkedList
的區別?分別用在什麼場景?ArrayList
查詢快而增刪慢?Arrays.asList
方法後的List能夠擴容嗎?modCount
在非線程安全集合中的做用?ArrayList
和LinkedList
的區別、優缺點以及應用場景ArrayList
是由動態再分配的Object[]
數組做爲底層結構,可設置null
值,是非線程安全的。node
//默認的空的數組,在構造方法初始化一個空數組的時候使用 private static final Object[] EMPTY_ELEMENTDATA = {}; //使用默認size大小的空數組實例 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList底層存儲數據就是經過數組的形式,ArrayList長度就是數組的長度。 transient Object[] elementData; //arrayList的大小 private int size;
那麼ArrayList
底層數據結構是什麼呢?面試
很明顯,使用動態再分配的Object[]
數組做爲ArrayList
底層數據結構了,既然是使用數組實現的,那麼數組特色就能說明爲何ArrayList查詢快而增刪慢?數組
由於數組是根據下標查詢不須要比較,查詢方式爲:首地址+(元素長度*下標),基於這個位置讀取相應的字節數就能夠了,因此很是快;可是增刪會帶來元素的移動,增長數據會向後移動,刪除數據會向前移動,致使其效率比較低。安全
Collection
類型的構造器//帶有初始化容量的構造方法 public ArrayList(int initialCapacity) { //參數大於0,elementData初始化爲initialCapacity大小的數組 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; //參數小於0,elementData初始化爲空數組 } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; //參數小於0,拋出異常 } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } //無參構造方法 public ArrayList() { //在1.7之後的版本,先構造方法中將elementData初始化爲空數組DEFAULTCAPACITY_EMPTY_ELEMENTDATA //當調用add方法添加第一個元素的時候,會進行擴容,擴容至大小爲DEFAULT_CAPACITY=10 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
那麼ArrayList
默認大小是多少?數據結構
從無參構造方法中能夠看出,一開始默認爲一個空的實例elementData
爲上面的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,當添加第一個元素的時候會進行擴容,擴容大小就是上面的默認容量DEFAULT_CAPACITY
爲10dom
boolean add(E)
:默認直接在末尾添加元素void add(int,E)
:在特定位置添加元素,也就是插入元素boolean addAll(Collection<? extends E> c)
:添加集合boolean addAll(int index, Collection<? extends E> c)
:在指定位置後添加集合public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
經過ensureCapacityInternal
方法爲肯定容量大小方法。在添加元素以前須要肯定數組是否能容納下,size
是數組中元素個數,添加一個元素size+1。而後再數組末尾添加元素。函數
其中,ensureCapacityInternal
方法包含了ArrayList
擴容機制grow
方法,當前容量沒法容納下數據時1.5倍擴容,進行:性能
private void ensureCapacityInternal(int minCapacity) { //判斷當前的數組是否爲默認設置的空數據,是否取出最小容量 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } //包括擴容機制grow方法 ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { //記錄着集合的修改次數,也就每次add或者remove它的值都會加1 modCount++; //當前容量容納不下數據時(下標超過期),ArrayList擴容機制:擴容原來的1.5倍 if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //ArrayList擴容機制:擴容原來的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
ArrayList
是如何擴容的?this
根據當前的容量容納不下新增數據時,ArrayList
會調用grow
進行擴容:
//至關於int newCapacity = oldCapacity + oldCapacity/2 int newCapacity = oldCapacity + (oldCapacity >> 1);
擴容原來的1.5倍。
public void add(int index, E element) { //檢查index也就是插入的位置是否合理,是否存在數組越界 rangeCheckForAdd(index); //機制和boolean add(E)方法同樣 ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
elementData
中每一個元素都賦值爲null,等待垃圾回收將這個給回收掉,public E remove(int index) { //檢查下標是否超出數組長度,形成數組越界 rangeCheck(index); modCount++; E oldValue = elementData(index); //算出數組須要移動的元素數量 int numMoved = size - index - 1; if (numMoved > 0) //數組數據遷移,這樣會致使刪除數據時,效率會慢 System.arraycopy(elementData, index+1, elementData, index, numMoved); //將--size上的位置賦值爲null,讓gc(垃圾回收機制)更快的回收它。 elementData[--size] = null; // clear to let GC do its work //返回刪除的元素 return oldValue; }
爲何說ArrayList
刪除元素效率低?
由於刪除數據須要將數據後面的元素數據遷移到新增位置的後面,這樣致使性能降低不少,效率低。
public boolean remove(Object o) { //若是須要刪除數據爲null時,會讓數據從新排序,將null數據遷移到數組尾端 if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { //刪除數據,並遷移數據 fastRemove(index); return true; } } else { //循環刪除數組中object對象的值,也須要數據遷移 for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; }
能夠看出,arrayList
是能夠存放null
值。
LinkedList
是一個繼承於AbstractSequentialList
的雙向鏈表。它也能夠被當作堆棧、隊列或雙端隊列進行使用,並且LinkedList
也爲非線程安全, jdk1.6使用的是一個帶有 header
節頭結點的雙向循環鏈表, 頭結點不存儲實際數據 ,在1.6以後,就變動使用兩個節點first
、last
指向首尾節點。
//鏈表節點的個數 transient int size = 0; //鏈表首節點 transient Node<E> first; //鏈表尾節點 transient Node<E> last; //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; } }
一旦變量被transient
修飾,變量將再也不是對象持久化的一部分,該變量內容在序列化後沒法得到訪問
無參構造函數, 默認構造方法聲明也不作,first
和last
節點會被默認初始化爲null。
* /** Constructs an empty list. \*/* public LinkedList() {}
因爲LinkedList
由雙向鏈表做爲底層數據結構,所以其插入無非由三大種
add(E e)
、addLast(E e)
、addAll(Collection<? extends E> c)
addFirst(E e)
add(int index, E element)
能夠從源碼看出,在鏈表首尾添加元素很高效,在中間添加元素比較低效,首先要找到插入位置的節點,在修改先後節點的指針。
//經常使用的添加元素方法 public boolean add(E e) { //使用尾插法 linkLast(e); return true; } //在鏈表尾部添加元素 public void addLast(E e) { linkLast(e); } //在鏈表尾端添加元素 void linkLast(E e) { //尾節點 final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; //判斷是不是第一個添加的元素 //若是是將新節點賦值給last //若是不是把原首節點的prev設置爲新節點 if (l == null) first = newNode; else l.next = newNode; size++; //將集合修改次數加1 modCount++; }
public void addFirst(E e) { //在鏈表頭插入指定元素 linkFirst(e); } private void linkFirst(E e) { //獲取頭部元素,首節點 final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; //鏈表頭部爲空,(也就是鏈表爲空) //插入元素爲首節點元素 // 不然就更新原來的頭元素的prev爲新元素的地址引用 if (f == null) last = newNode; else f.prev = newNode; // size++; modCount++; }
當index
不爲首尾的的時候,實際就在鏈表中間插入元素。
// 做用:在指定位置添加元素 public void add(int index, E element) { // 檢查插入位置的索引的合理性 checkPositionIndex(index); if (index == size) // 插入的狀況是尾部插入的狀況:調用linkLast()。 linkLast(element); else // 插入的狀況是非尾部插入的狀況(中間插入):linkBefore linkBefore(element, node(index)); } private void checkPositionIndex(int index) { if (!isPositionIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private boolean isPositionIndex(int index) { return index >= 0 && index <= size; } void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; // 獲得插入位置元素的前繼節點 final Node<E> newNode = new Node<>(pred, e, succ); // 建立新節點,其前繼節點是succ的前節點,後接點是succ節點 succ.prev = newNode; // 更新插入位置(succ)的前置節點爲新節點 if (pred == null) // 若是pred爲null,說明該節點插入在頭節點以前,要重置first頭節點 first = newNode; else // 若是pred不爲null,那麼直接將pred的後繼指針指向newNode便可 pred.next = newNode; size++; modCount++; }
刪除和插入同樣,其實本質也是隻有三大種方式,
removeFirst()
removeLast()
remove(Object o)
、remove(int index)
在首尾節點刪除很高效,刪除中間元素比較低效要先找到節點位置,再修改先後指針指引。
remove(int index)
和remove(Object o)
都是使用刪除指定節點的unlink
刪除元素
public boolean remove(Object o) { //由於LinkedList容許存在null,因此須要進行null判斷 if (o == null) { //從首節點開始遍歷 for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { //調用unlink方法刪除指定節點 unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } //刪除指定位置的節點,其實和上面的方法差很少 //經過node方法得到指定位置的節點,再經過unlink方法刪除 public E remove(int index) { checkElementIndex(index); return unlink(node(index)); } //刪除指定節點 E unlink(Node<E> x) { //獲取x節點的元素,以及它上一個節點,和下一個節點 final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; //若是x的上一個節點爲null,說明是首節點,將x的下一個節點設置爲新的首節點 //不然將x的上一節點設置爲next,將x的上一節點設爲null if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } //若是x的下一節點爲null,說明是尾節點,將x的上一節點設置新的尾節點 //不然將x的上一節點設置x的上一節點,將x的下一節點設爲null if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } //將x節點的元素值設爲null,等待垃圾收集器收集 x.item = null; //鏈表節點個數減1 size--; //將集合修改次數加1 modCount++; //返回刪除節點的元素值 return element; }
//刪除首節點 public E remove() { return removeFirst(); } //刪除首節點 public E removeFirst() { final Node<E> f = first; //若是首節點爲null,說明是空鏈表,拋出異常 if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } //刪除首節點 private E unlinkFirst(Node<E> f) { //首節點的元素值 final E element = f.item; //首節點的下一節點 final Node<E> next = f.next; //將首節點的元素值和下一節點設爲null,等待垃圾收集器收集 f.item = null; f.next = null; // help GC //將next設置爲新的首節點 first = next; //若是next爲null,說明說明鏈表中只有一個節點,把last也設爲null //不然把next的上一節點設爲null if (next == null) last = null; else next.prev = null; //鏈表節點個數減1 size--; //將集合修改次數加1 modCount++; //返回刪除節點的元素值 return element; }
//刪除尾節點 public E removeLast() { final Node<E> l = last; //若是首節點爲null,說明是空鏈表,拋出異常 if (l == null) throw new NoSuchElementException(); return unlinkLast(l); } private E unlinkLast(Node<E> l) { //尾節點的元素值 final E element = l.item; //尾節點的上一節點 final Node<E> prev = l.prev; //將尾節點的元素值和上一節點設爲null,等待垃圾收集器收集 l.item = null; l.prev = null; // help GC //將prev設置新的尾節點 last = prev; //若是prev爲null,說明說明鏈表中只有一個節點,把first也設爲null //不然把prev的下一節點設爲null if (prev == null) first = null; else prev.next = null; //鏈表節點個數減1 size--; //將集合修改次數加1 modCount++; //返回刪除節點的元素值 return element; }
其餘方法也是相似的,好比查詢方法 LinkedList
提供了get
、getFirst
、getLast
等方法獲取節點元素值。
modCount
屬性表明爲結構性修改( 改變list的size大小、以其餘方式改變他致使正在進行迭代時出現錯誤的結果)的次數,該屬性被Iterato
r以及ListIterator
的實現類所使用,且不少非線程安全使用modCount
屬性。
初始化迭代器時會給這個modCount賦值,若是在遍歷的過程當中,一旦發現這個對象的modCount和迭代器存儲的modCount不同,Iterator
或者ListIterator
將拋出ConcurrentModificationException
異常,
這是jdk在面對迭代遍歷的時候爲了不不肯定性而採起的 fail-fast(快速失敗)原則:
在線程不安全的集合中,若是使用迭代器的過程當中,發現集合被修改,會拋出ConcurrentModificationExceptions
錯誤,這就是fail-fast機制。對集合進行結構性修改時,modCount
都會增長,在初始化迭代器時,modCount
的值會賦給expectedModCount
,在迭代的過程當中,只要modCount
改變了,int expectedModCount = modCount
等式就不成立了,迭代器檢測到這一點,就會拋出錯誤:urrentModificationExceptions
。
區別:
ArrayList
是實現了基於動態數組的數據結構,LinkedList
是基於鏈表結構。get
和set
方法查詢元素,ArrayList
要優於LinkedList
,由於LinkedList
循環鏈表尋找元素。add
和remove
,LinkedList
比較高效,由於ArrayList
要移動數據。優缺點:
ArrayList
和LinkedList
而言,在末尾增長一個元素所花的開銷都是固定的。對ArrayList
而言,主要是在內部數組中增長一項,指向所添加的元素,偶爾可能會致使對數組從新進行分配;而對LinkedList
而言,這個開銷是 統一的,分配一個內部Entry
對象。ArrayList
集合中添加或者刪除一個元素時,當前的列表移動元素後面全部的元素都會被移動。而LinkedList
集合中添加或者刪除一個元素的開銷是固定的。LinkedList
集合不支持 高效的隨機隨機訪問(RandomAccess
),由於可能產生二次項的行爲。ArrayList
的空間浪費主要體如今在list列表的結尾預留必定的容量空間,而LinkedList
的空間花費則體如今它的每個元素都須要消耗至關的空間應用場景:
ArrayList
使用在查詢比較多,可是插入和刪除比較少的狀況,而LinkedList
用在查詢比較少而插入刪除比較多的狀況
各位看官還能夠嗎?喜歡的話,動動手指點個💗,點個關注唄!!謝謝支持!
歡迎掃碼關注,原創技術文章第一時間推出
![]()