前言
在一開始基礎面的時候,很多面試官可能會問List集合一些基礎知識,比如:
-
ArrayList
預設大小是多少,是如何擴容的? -
ArrayList
和LinkedList
的底層資料結構是什麼? -
ArrayList
和LinkedList
的區別?分別用在什麼場景? -
為什麼說
ArrayList
查詢快而增刪慢? -
Arrays.asList
-
modCount
在非執行緒安全集合中的作用? -
ArrayList
和LinkedList
的區別、優缺點以及應用場景
ArrayList(1.8)
ArrayList
是由動態再分配的Object[]
陣列作為底層結構,可設定null
值,是非執行緒安全的。
ArrayList成員屬性
//預設的空的陣列,在構造方法初始化一個空陣列的時候使用 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查詢快而增刪慢?
因為陣列是根據下標查詢不需要比較,查詢方式為:首地址+(元素長度*下標),基於這個位置讀取相應的位元組數就可以了,所以非常快;但是增刪會帶來元素的移動,增加資料會向後移動,刪除資料會向前移動,導致其效率比較低。
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
為10
ArrayList的Add方法
-
boolean add(E)
:預設直接在末尾新增元素 -
void add(int,E)
:在特定位置新增元素,也就是插入元素 -
boolean addAll(Collection<? extends E> c)
:新增集合 -
boolean addAll(int index, Collection<? extends E> c)
:在指定位置後新增集合
boolean add(E)
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
是如何擴容的?
根據當前的容量容納不下新增資料時,ArrayList
會呼叫grow
進行擴容:
//相當於int newCapacity = oldCapacity + oldCapacity/2 int newCapacity = oldCapacity + (oldCapacity >> 1);
擴容原來的1.5倍。
void add(int,E)
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++; }
ArrayList的刪除方法
-
remove(int):通過刪除指定位置上的元素,
-
remove(Object):根據元素進行刪除,
-
clear():將
elementData
中每個元素都賦值為null,等待垃圾回收將這個給回收掉, -
removeAll(collection c):批量刪除。
remove(int)
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
刪除元素效率低?
因為刪除資料需要將資料後面的元素資料遷移到新增位置的後面,這樣導致效能下降很多,效率低。
remove(Object)
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(1.8)
LinkedList
是一個繼承於AbstractSequentialList
的雙向連結串列。它也可以被當做堆疊、佇列或雙端佇列進行使用,而且LinkedList
也為非執行緒安全, jdk1.6使用的是一個帶有 header
節頭結點的雙向迴圈連結串列, 頭結點不儲存實際資料 ,在1.6之後,就變更使用兩個節點first
、last
指向首尾節點。
LinkedList的主要屬性
//連結串列節點的個數 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
修飾,變數將不再是物件持久化的一部分,該變數內容在序列化後無法獲得訪問
LinkedList構造方法
無參建構函式, 預設構造方法宣告也不做,first
和last
節點會被預設初始化為null。
* /** Constructs an empty list. \*/* public LinkedList() {}
LinkedList插入
由於LinkedList
由雙向連結串列作為底層資料結構,因此其插入無非由三大種
-
尾插:
add(E e)
、addLast(E e)
、addAll(Collection<? extends E> c)
-
頭插:
addFirst(E e)
-
中插:
add(int index, E element)
可以從原始碼看出,在連結串列首尾新增元素很高效,在中間新增元素比較低效,首先要找到插入位置的節點,在修改前後節點的指標。
尾插-add(E e)和addLast(E e)
//常用的新增元素方法 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++; }
頭插-addFirst(E e)
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++; }
中插-add(int index, E element)
當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++; }
LinkedList 刪除
刪除和插入一樣,其實本質也是隻有三大種方式,
-
刪除首節點:
removeFirst()
-
刪除尾節點:
removeLast()
-
刪除中間節點 :
remove(Object o)
、remove(int index)
在首尾節點刪除很高效,刪除中間元素比較低效要先找到節點位置,再修改前後指標指引。
刪除中間節點-remove(int index)和remove(Object o)
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; }
刪除首節點-removeFirst()
//刪除首節點 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; }
刪除尾節點-removeLast()
//刪除尾節點 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屬性的作用?
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的區別、優缺點以及應用場景
區別:
-
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
用在查詢比較少而插入刪除比較多的情況
各位看官還可以嗎?喜歡的話,動動手指點個?,點個關注唄!!謝謝支援!
歡迎掃碼關注,原創技術文章第一時間推出