Java-ArrayList & LinkedList的原始碼對比分析

小肖愛吃肉發表於2020-11-16

一、引用

在進入正文之前,先了解下List集合累的介面和實現類的繼承關係:
在這裡插入圖片描述
(圖片來源:極客時間-Java效能調優實戰 05)

從這個圖片可以清晰的看到,ArrayList和LinkedList都繼承了AbstractList抽象類,上層實現了List介面,又根據自我定位,實現不同的功能

二、ArrayList的實現

ArrayList是基於陣列實現的,在底層維護了一個 Object 類的陣列用來存放元素

2.1 ArrayList實現類

ArrayList的類定義:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
  1. 實現 List 介面,繼承了 AbstractList 抽象類,這個是很好理解的,ArrayList作為List集合類家族的一部分,可以更好的將公用方法抽象給上層
  2. 實現Cloneable和Serializable介面 ,可以進行克隆和序列化操作
  3. 實現RandomAccess介面,這個介面會相對陌生點,點進去官方給的解釋是,RandomAccess是一個標記介面,是一個空介面,僅起到標記的作用(類似Serializable介面)
    public interface RandomAccess {
    }
    

List實現這個介面表明這個類能實現 快速隨機訪問。該介面的主要目的是允許通用演算法更改其行為,以便在應用於隨機訪問或順序訪問列表時提供良好的效能

而後文件裡給了說明,如果實現了RandomAccess介面,對List做查詢操作時使用for迴圈的方式,否則使用迭代器的方式。這樣做的原因做了RandomAccess介面標記的LIst實現類底層資料結構使用for迴圈效率更高(比如ArrayList),沒有RandomAccess介面標記的使用迭代器效率更高(比如LinkedList,下文可以看到LinkedList沒有實現RandomAccess介面)

舉個簡單的小例子,比如Collections類裡的二分查詢方法,就是用是否標記了RandomAccess介面來區分用哪種方法實現的:

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
  if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
    return Collections.indexedBinarySearch(list, key);  // for迴圈方法
  else
    return Collections.iteratorBinarySearch(list, key); // Iterator迴圈方法
}

2.2 ArrayList建構函式

ArrayList實現了三種建構函式用於不同情形下的物件建立

  1. 無參建構函式
    public ArrayList() {
      this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    構造了一個空陣列,在首次新增元素的時候才給資料設定大小
  2. 給定初始容量的建構函式
    public ArrayList(int initialCapacity) {
      if (initialCapacity > 0) {
         // initialCapacity大於0,陣列的大小為initialCapacity值
        this.elementData = new Object[initialCapacity];
      } else if (initialCapacity == 0) {
        // initialCapacity等於0,生成預設空陣列EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
      } else {
        // 其他情況,報非法引數異常-》陣列的大小必須大於等於0
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
      }
    }
    
    通過引數 initialCapacity 可設定List陣列的初始大小,這樣做的好處是當 ArrayList 新增元素時,如果所儲存的元素已經超過其已有大小,它會計算元素大小後再進行動態擴容,陣列的擴容會導致整個陣列進行一次記憶體複製。因此,我們在初始化 ArrayList 時,可以通過第一個建構函式合理指定陣列初始大小,這樣有助於減少陣列的擴容次數,從而提高系統效能
  3. 傳入Collection物件轉化成ArrayList
    public ArrayList(Collection<? extends E> c) {
      Object[] a = c.toArray();
      if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
          elementData = a;
        } else {
          elementData = Arrays.copyOf(a, size, Object[].class);
        }
      } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
      }
    }
    

將 Collection 轉化為陣列並賦值給 elementData,把 elementData 中元素的個數賦值給 size。 如果 size 不為零,則判斷 elementData 的 class 型別是否為 Object[],不是的話則做一次轉換。 如果 size 為零,則把 EMPTY_ELEMENTDATA 賦值給 elementData,相當於new ArrayList(0)

2.3 ArrayList屬性

ArrayList主要包含了三個屬性:

  1. elementData :底層陣列,用來儲存資料
  2. DEFAULT_CAPACITY :陣列的預設初始化容量 10
  3. size :當前的陣列大小,用來表示當前陣列包含了多少個元素
    transient Object[] elementData; // non-private to simplify nested class access
    private static final int DEFAULT_CAPACITY = 10;
    private int size;
    

2.4 ArrayList新增元素

ArrayList提供了兩種新增元素的方法,第一種:直接將元素新增到陣列尾部;第二種:將元素新增到指定位置

/**
 * 直接將元素新增到陣列尾部
 */
public boolean add(E e) {
  // 1.判斷當前陣列容量是否夠用,不夠的話進行陣列擴容操作
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  // 2.將元素新增到陣列尾部,size加1
  elementData[size++] = e;
  return true;
}/**
 * 將元素新增到指定位置
 */
public void add(int index, E element) {
  // 1.判斷指定位置是否在陣列包含範圍內
  rangeCheckForAdd(index);
  // 2.判斷當前陣列容量是否夠用,不夠的話進行陣列擴容操作
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  // 3.將指定位置開始的元素向後挪動一位
  System.arraycopy(elementData, index, elementData, index + 1,
                   size - index);
  // 4.將元素新增到指定位置上
  elementData[index] = element;
  size++;
}

從程式碼裡可以看出,兩種新增方式都執行了 ensureCapacityInternal 方法,判斷陣列的容量情況,具體看下這個方法是如何實現的呢:

判斷當前陣列是否是空陣列EMPTY_ELEMENTDATA,如果是的話,判斷預設值(10)和傳入的容量(size+1)誰大,就返回哪個,如果不是空陣列的話,直接返回(size+1),這一步是對初始化為空陣列的情況進行特殊處理

判斷傳入容量(size+1)是否大於當前的陣列大小,如果是的話進行擴容操作(grow)

擴容操作是將容量擴充到原始容量的1.5倍大小(oldCapacity + (oldCapacity >> 1)),如果還是不夠,就將容量給定為當前傳入的容量(size+1)

這裡需要判斷下是否有記憶體溢位的問題,當容量達到允許的最大值(MAX_ARRAY_SIZE)的時候,將陣列容量給定為int最大值

擴容後需要將原陣列重新分配到新的記憶體地址中

/**
 * 計算陣列容量,從傳入的容量和預設容量(10)裡面去較大的
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
  if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    return Math.max(DEFAULT_CAPACITY, minCapacity);
  }
  return minCapacity;
}/**
 * 計算陣列容量,從傳入的容量和預設容量(10)裡面去較大的
 */
private void ensureCapacityInternal(int minCapacity) {
  ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}/**
 * 計算陣列容量,如果傳入容量比當前陣列已有元素數量小,需要進行擴容操作
 */
private void ensureExplicitCapacity(int minCapacity) {
  modCount++;// overflow-conscious code
  if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}/**
 * 陣列擴容
 */
private void grow(int minCapacity) {
  // 計算擴容後的陣列容量
  // overflow-conscious code
  int oldCapacity = elementData.length;
  // 擴容後新的容量是原容量的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);
}/**
 * 資料容量大小邊界處理
 */
private static int hugeCapacity(int minCapacity) {
  if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();
  return (minCapacity > MAX_ARRAY_SIZE) ?
    Integer.MAX_VALUE :
  MAX_ARRAY_SIZE;
}/**
 * 資料允許的最大容量
 */
 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

從程式碼裡可以看到,新增元素到任意位置,會導致在該位置後的所有元素都需要重新排列,而將元素新增到陣列的末尾,在沒有發生擴容的前提下,是不會有元素複製排序過程的。所以直接將元素新增的陣列的末尾效率會更高。

2.5 ArrayList查詢元素

ArrayList在資料查詢上的效率很高,只需要O(1)的時間複雜度就能獲取資料,傳入需要元素的下標index,返回資料中的對應元素即可

public E get(int index) {
  // 檢查index是否越界,
  rangeCheck(index);
  // 返回陣列對應下標元素值
  return elementData(index);
}private void rangeCheck(int index) {
  if (index >= size)
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}@SuppressWarnings("unchecked")
E elementData(int index) {
  return (E) elementData[index];
}

2.6 ArrayList刪除元素

ArrayList刪除元素的程式碼邏輯和新增元素到任意位置的方法類似,在每一次有效的刪除元素操作之後,都要進行陣列的重組,並且刪除的元素位置越靠前,陣列重組的開銷就越大

  1. 判斷index是否合法(有沒有下標越界)
  2. 獲取要刪除的元素
  3. 將要刪除資料下標往後的元素向前移動一位
  4. 將最後一位置零
  5. 返回被刪除的元素
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);
  elementData[--size] = null; // clear to let GC do its workreturn oldValue;
}

三、LinkedList的實現

雖然LinkedList和ArrayList都同屬於List集合下的,但在實現上卻有很大的區別。LinkedList是基於雙向連結串列實現的,LinkedList 定義了一個 Node 結構用來儲存資料,Node裡維護了三個屬性:

  1. item:用來儲存元素內容
  2. next:指向下一個節點地址
  3. prev:指向上一個節點地址
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;
  }
}

3.1 LinkedList實現類

  1. 實現了 List 介面,有了List型別的特點
  2. 實現了 Deque 介面,有了Queue型別的特點
  3. 實現了Cloneable 和 Serializable 介面,可以實現克隆和序列化

⚠️ 由於 LinkedList 儲存資料的記憶體地址是不連續的,而是通過指標來定位不連續地址,因此,LinkedList 不支援隨機快速訪問,LinkedList 也就不能實現 RandomAccess 介面

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{

3.2 LinkedList建構函式

由於LinkedList是雙向連結串列,不像ArrayList那樣可以提前設定容量大小,所以LinkedList只有兩個建構函式,一個是空構造器,一個是將傳入的Collection資料全部存入

public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
  this();
  addAll(c);
}

3.3 LinkedList屬性

LinkedList同樣也包含了三個屬性:

  1. size :記錄前LinkedList儲存的元素節點個數
  2. first :雙向連結串列的頭節點
  3. last :雙向連結串列的尾節點

在JDK1.7以後使用了first/last節點來表示雙向連結串列,而不是之前的使用head節點,這樣的好處有:

  1. first/last 屬效能更清晰地表達連結串列的鏈頭和鏈尾概念
  2. first/last 方式可以在初始化 LinkedList 的時候節省 new 一個 Entry
  3. first/last 方式最重要的效能優化是鏈頭和鏈尾的插入刪除操作更加快捷了

除此之外可以看到這三個屬性都被 transient 修飾了,原因很簡單,我們在序列化的時候不會只對頭尾進行序列化,所以 LinkedList 也是自行實現 readObject 和 writeObject 進行序列化與反序列化

transient int size = 0;/**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
transient Node<E> first;/**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
transient Node<E> last;

3.4 LinkedList新增元素

LinkedList提供了多種插入資料的方式:

預設的add(E element)是將資料新增到連結串列的尾部,首先建立一個新的節點物件,將之前的last指標指向物件的next節點指向新物件,將last指標指向新物件,完成尾插

public boolean add(E e) {
  linkLast(e);
  return true;
}/**
 * Links e as last element.
 */
void linkLast(E e) {
  // 將原先的last指標指向的物件放到臨時變數裡
  final Node<E> l = last;
  // 建立一個新的節點物件,傳入l節點作為新節點的前置節點
  final Node<E> newNode = new Node<>(l, e, null);
  // last指標指向新物件
  last = newNode;
  // 判斷下,如果之前的節點為空,證明是空連結串列,這是第一次插入,將first指標指向新節點
  if (l == null)
    first = newNode;
  else
    // 原last指標指向的節點指向新的節點(新節點掛鏈到原連結串列上了)
    l.next = newNode;
  size++;
  modCount++;
}

addLast(E e)和預設的add相同,實現裡就一行程式碼,呼叫了linkLast方法

addFirst(E e)和addLast正好相反,是從頭部插入資料

/**
 * Links e as first element.
 */
private void linkFirst(E e) {
  final Node<E> f = first;
  final Node<E> newNode = new Node<>(null, e, f);
  first = newNode;
  if (f == null)
    last = newNode;
  else
    f.prev = newNode;
  // 連結串列個數+1
  size++;
  modCount++;
}

add(int index, E element)方法是將元素插入到指定的位置上:

  1. 首先,判斷index是否在合法範圍內
  2. 遍歷連結串列找到index所在的節點,遍歷的時候會判斷index是在連結串列的前半部分還是後半部分來決定從first開始還是last開始
  3. 將新節點插入到index節點的前一個位置
  4. 將元素新增到任意兩個元素的中間位置,新增元素操作只會改變前後元素的前後指標,指標將會指向新增的新元素,所以相比 ArrayList 的新增操作來說,LinkedList 的效能優勢明顯。
/**
 * 指定位置的插入
 */
public void add(int index, E element) {
  // 校驗index是否合法
  checkPositionIndex(index);
  // 如果index剛好等於size,則直接尾插
  if (index == size)
    linkLast(element);
  else
    linkBefore(element, node(index));
}/**
 * 校驗index是否合法
 */
private boolean isPositionIndex(int index) {
  return index >= 0 && index <= size;
}
private void checkPositionIndex(int index) {
  if (!isPositionIndex(index))
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}/**
 * 將新節點插入到當前節點(succ)的前一個位置上
 */
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.prev = newNode;
  // 如果前置節點為空,代表是連結串列頭了
  if (pred == null)
    first = newNode;
  else
    pred.next = newNode;
  size++;
  modCount++;
}

3.5 LinkedList查詢元素

LinkedList查詢元素和將新節點插入指定位置的邏輯是一樣的,實際上將元素插入指定位置就是先查詢到元素,再進行插入操作。同樣,這裡會先對index進行合法性校驗,再通過index計算是從前向向後還是從後向前對連結串列進行遍歷獲取到對應資料

public E get(int index) {
  checkElementIndex(index);
  return node(index).item;
}
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;
  }
}

3.6 LinkedList刪除元素

LinkedList提供了兩種刪除資料的方法,第一種是remove(int index),刪除指定下標的元素,同查詢元素邏輯一樣,先遍歷查詢到index對應的下標節點,然後將這個節點的前置節點(prev)的next關聯到這個節點的後置節點(next),將當前節點對應的屬性值釋放(賦值為null,便於JVM回收),這裡要注意對邊界值的處理

/**
 * 刪除指定下標的元素
 */    
public E remove(int index) {
  checkElementIndex(index);
  return unlink(node(index));
}/**
 * 刪除指定下標的元素
 */ 
E unlink(Node<E> x) {
  // assert x != null;
  final E element = x.item;
  final Node<E> next = x.next;
  final Node<E> prev = x.prev;if (prev == null) {
    first = next;
  } else {
    prev.next = next;
    x.prev = null;
  }if (next == null) {
    last = prev;
  } else {
    next.prev = prev;
    x.next = null;
  }
​
  x.item = null;
  // 處理完後連結串列個數-1
  size--;
  modCount++;
  return element;
}

另一個刪除方法是remove(Object o),刪除指定的元素,由於無法用index定位,所以這種刪除方式只能每次動first開始遍歷,將遇到的所有存的是指定元素值的節點全部刪除,這裡會對null進行特殊處理,如果入參為null的話就刪除所有元素值為null的節點

public boolean remove(Object o) {
  if (o == null) {
    for (Node<E> x = first; x != null; x = x.next) {
      if (x.item == null) {
        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;
}

此外,LinkedList還提供了peek()和poll()系列方法,用來實現Queue的特性,這裡就不展開論述了

四、總結

對比項ArrayListLinkedList
實現介面實現 List 介面,具有List型別特點實現List 和 Deque介面,兼有List和Queue型別特點
克隆和序列化可以實現 ✅可以實現 ✅
隨機快速訪問支援 ✅不支援 ❌
元素遍歷for迴圈效率更高迭代器效率更高
資料結構陣列 (Object[]雙向連結串列 (Node)
擴容擴容1.5倍連結串列無需擴容
記憶體佔用連續的記憶體空間不連續的記憶體空間,額外增加prev和next屬性空間

寫在最後 歡迎關注微信公眾號【小肖愛吃肉】和你一起記錄生活的小美好
在這裡插入圖片描述

相關文章