Java集合乾貨1——ArrayList原始碼分析

冰洋發表於2018-01-13

前言

在之前的文章中我們提到過ArrayList,ArrayList可以說是每一個學java的人使用最多最熟練的集合了,但是知其然不知其所以然。關於ArrayList的具體實現,一些基本的都也知道,譬如陣列實現,執行緒不安全等等,但是更加具體的就很少去了解了,例如:初始化的長度,擴容等。

本篇主要通過一些對原始碼的分析,講解幾個ArrayList常見的方法,以及和Vector的區別。

ArrayList

定義

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
複製程式碼

ArrayList實際上是一個動態陣列,容量可以動態的增長,其繼承了AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些介面。

RandomAccess介面,被List實現之後,表明List提供了隨機訪問功能,也就是通過下標獲取元素物件的功能。

1.RandomAccess介面,標記介面,表明List提供了隨機訪問功能,也就是通過下標獲取元素物件的功能。之所以是標記介面,是該類本來就具有某項能力,使用介面對其進行標籤化,便於其他的類對其進行識別(instanceof)。 2.ArrayList陣列實現,本身就有通過下標隨機訪問任意元素的功能。那麼需要細節上注意的就是隨機下標訪問和順序下標訪問(LinkedList)的不同了。也就是為什麼LinkedList最好不要使用迴圈遍歷,而是用迭代器遍歷的原因。 3.實現RandomAccess同時意味著一些演算法可以通過型別判斷進行一些針對性優化,例子有Collections的shuffle方法,原始碼就不貼上了,簡單說就是,如果實現RandomAccess介面就下標遍歷,反之迭代器遍歷 實現了Cloneable, java.io.Serializable意味著可以被克隆和序列化。

初始化

在使用中我們經常需要new出來各種泛型的ArrayList,那麼在初始化過程是怎樣的呢?

如下一行程式碼,建立一個ArrayList物件

List<Person> list = new ArrayList<>();
複製程式碼

我們來看原始碼,是如何初始化的,找到構造方法

//無參構造方法
public ArrayList() {
  super();
  this.elementData = EMPTY_ELEMENTDATA;
}
複製程式碼

看到這些程式碼的時候,我也是不解的,elementData和EMPTY_ELEMENTDATA是什麼啊?但是很明顯EMPTY_ELEMENTDATA是一個常量,追本溯源我們去看一下成員屬性。

//如果是無參構造方法建立物件的話,ArrayList的初始化長度為10,這是一個靜態常量
private static final int DEFAULT_CAPACITY = 10;

//在這裡可以看到我們不解的EMPTY_ELEMENTDATA實際上是一個空的物件陣列
    private static final Object[] EMPTY_ELEMENTDATA = {};

//儲存ArrayList資料的物件陣列緩衝區 空的ArrayList的elementData = EMPTY_ELEMENTDATA 這就是為什麼說ArrayList底層是陣列實現的了。elementData的初始容量為10,大小會根據ArrayList容量的增長而動態的增長。
    private transient Object[] elementData;
//集合的長度
    private int size;
複製程式碼

執行完構造方法,如下圖

2018-01-11_110237

可以說ArrayList的作者真的是很貼心,連快取都處理好了,多次new出來的新物件,都執行同一個引用。

新增方法add

add(E e)
 /**
     * Appends the specified element to the end of this list.
     */
//增加元素到集合的最後
public boolean add(E e) {
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  //因為++運算子的特點 先使用後運算  這裡實際上是
  //elementData[size] = e
  //size+1
  elementData[size++] = e;
  return true;
}
複製程式碼

先不管ensureCapacityInternal的話,這個方法就是將一個元素增加到陣列的size++位置上。

再說回ensureCapacityInternal,它是用來擴容的,準確說是用來進行擴容檢查的。下面我們來看一下整個擴容的過程

//初始長度是10,size預設值0,假定新增的是第一個元素,那麼minCapacity=1 這是最小容量 如果小於這個容量就會報錯
//如果底層陣列就是預設陣列,那麼選一個大的值,就是10
private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
		//呼叫另一個方法ensureExplicitCapacity
        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
      //記錄修改的次數
        modCount++;

        // overflow-conscious code
      //如果傳過來的值大於初始長度 執行grow方法(引數為傳過來的值)擴容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
//真正的擴容
 private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
   //新的容量是在原有的容量基礎上+50% 右移一位就是二分之一
        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:
   //這裡是重點 呼叫工具類Arrays的copyOf擴容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

//Arrays
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
  T[] copy = ((Object)newType == (Object)Object[].class)
    ? (T[]) new Object[newLength]
    : (T[]) Array.newInstance(newType.getComponentType(), newLength);
  System.arraycopy(original, 0, copy, 0,
                   Math.min(original.length, newLength));
  return copy;
}


複製程式碼
add(int index, E element)

新增到指定的位置

public void add(int index, E element) {
  //判斷索引是否越界,如果越界就會丟擲下標越界異常
  rangeCheckForAdd(index);
//擴容檢查
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  //將指定下標空出 具體作法就是index及其後的所有元素後移一位
  System.arraycopy(elementData, index, elementData, index + 1,size - index);
  //將要新增元素賦值到空出來的指定下標處
  elementData[index] = element;
  //長度加1
  size++;
}
//判斷是否出現下標是否越界
private void rangeCheckForAdd(int index) {
  //如果下標超過了集合的尺寸 或者 小於0就是越界  
  if (index > size || index < 0)
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
複製程式碼
remove(int index)

ArrayList支援兩種刪除元素的方式

  1. remove(int index) 按照下標刪除 常用
  2. remove(Object o) 按照元素刪除 會刪除和引數匹配的第一個元素

下面我們看一下ArrayList的實現

 /**
 移除list中指定位置的元素
     * Removes the element at the specified position in this list.
     所有後續元素左移 下標減1
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *引數是要移除元素的下標
     * @param index the index of the element to be removed
     返回值是被移除的元素
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
public E remove(int index) {
  //下標越界檢查
  rangeCheck(index);
//修改次數統計
  modCount++;
  //獲取這個下標上的值
  E oldValue = elementData(index);
//計算出需要移動的元素個數 (因為需要將後續元素左移一位 此處計算出來的是後續元素的位數)
  int numMoved = size - index - 1;
  //如果這個值大於0 說明後續有元素需要左移  是0說明被移除的物件就是最後一位元素
  if (numMoved > 0)
    //索引index只有的所有元素左移一位  覆蓋掉index位置上的元素
    System.arraycopy(elementData, index+1, elementData, index,numMoved);
 // 將最後一個元素賦值為null  這樣就可以被gc回收了
  elementData[--size] = null; // clear to let GC do its work
//返回index位置上的元素
  return oldValue;
}

//移除特定元素
public boolean remove(Object o) {
  //如果元素是null 遍歷陣列移除第一個null
  if (o == null) {
    for (int index = 0; index < size; index++)
      if (elementData[index] == null) {
        //遍歷找到第一個null元素的下標 呼叫下標移除元素的方法
        fastRemove(index);
        return true;
      }
  } else {
    //找到元素對應的下標 呼叫下標移除元素的方法
    for (int index = 0; index < size; index++)
      if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
      }
  }
  return false;
}

//按照下標移除元素
private void fastRemove(int index) {
  modCount++;
  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 work
}
複製程式碼
ArrayList總結
  1. 底層陣列實現,使用預設構造方法初始化出來的容量是10
  2. 擴容的長度是在原長度基礎上加二分之一
  3. 實現了RandomAccess介面,底層又是陣列,get讀取元素效能很好
  4. 執行緒不安全,所有的方法均不是同步方法也沒有加鎖,因此多執行緒下慎用
  5. 順序新增很方便
  6. 刪除和插入需要複製陣列 效能很差(可以使用LinkindList)
為什麼ArrayList的elementData是用transient修飾的?

transient修飾的屬性意味著不會被序列化,也就是說在序列化ArrayList的時候,不序列化elementData。

為什麼要這麼做呢?

  1. elementData不總是滿的,每次都序列化,會浪費時間和空間
  2. 重寫了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 array length
       s.writeInt(elementData.length);
    // 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();
    }
}
複製程式碼

ArrayList和Vector的區別

標準答案
  1. ArrayList是執行緒不安全的,Vector是執行緒安全的
  2. 擴容時候ArrayList擴0.5倍,Vector擴1倍

那麼問題來了

ArrayList有沒有辦法執行緒安全?

Collections工具類有一個synchronizedList方法

可以把list變為執行緒安全的集合,但是意義不大,因為可以使用Vector

Vector為什麼是執行緒安全的?

老實講,拋開多執行緒 它倆區別沒多大,但是多執行緒下就不一樣了,那麼Vector是如何實現執行緒安全的,我們來看幾個關鍵方法

public synchronized boolean add(E e) {
  modCount++;
  ensureCapacityHelper(elementCount + 1);
  elementData[elementCount++] = e;
  return true;
}

public synchronized E remove(int index) {
  modCount++;
  if (index >= elementCount)
    throw new ArrayIndexOutOfBoundsException(index);
  E oldValue = elementData(index);

  int numMoved = elementCount - index - 1;
  if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
  elementData[--elementCount] = null; // Let gc do its work

  return oldValue;
}
複製程式碼

就程式碼實現上來說,和ArrayList並沒有很多邏輯上的區別,但是在Vector的關鍵方法都使用了synchronized修飾。

相關文章