ArrayList集合底層原理

炒燜煎糖板栗發表於2021-10-30

ArrayList集合特點及原始碼分析

ArrayList是List介面的實現類

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

繼承了AbstractList類,實現了Cloneable克隆介面、Serializable序列化介面、RandomAccess隨機訪問介面、List介面

特點:底層使用陣列實現的 查詢效率高,增刪慢,執行緒不安全、允許為空、可重複

public static void main(String[] args) {
    List<String> list=new ArrayList<String>();
    boolean bool=list.add("ylc");//Collection介面新增元素
    list.add(1,"ww");//根據索引新增元素
    String el=list.get(1);//根據索引獲取元素
    list.size();//獲取元素個數
    list.remove(2);//根據元素位置刪除
    list.remove("ylc");//刪除指定元素
    list.set(1,"yy");//替換元素
    list.clear();//清空集合
    list.isEmpty();//判斷元素是否為空
    list.contains("ylc");//判斷集合是否包含某個元素
    list.indexOf("ylc");//查詢所訴中所在的位置
    list.lastIndexOf("ylc");//元素最後出現的索引位置
    Object[] objects=list.toArray();//把集合轉化為object陣列
    for (int i = 0; i < objects.length; i++) {
        String str=(String) objects[i];
        System.out.println(str);
    }
    String[] strings=list.toArray(new String[list.size()]);//把集合轉化為指定型別陣列
    List<String> list2=new ArrayList<String>();
    list2.add("s");
    list.addAll(list2);//集合合併操作
    list.retainAll(list2);//集合交集操作 list儲存交集內容
    list.removeAll(list2);//刪除list中含有list2集合的元素
}

ArrayList原始碼分析

成員變數

private static final int DEFAULT_CAPACITY = 10;//陣列預設長度
private static final Object[] EMPTY_ELEMENTDATA = {};//給定一個空陣列
transient Object[] elementData;//儲存ArrayList元素的臨時陣列 不會被存到磁碟
private int size;//記錄陣列中元素的個數
protected transient int modCount = 0; // 集合陣列修改次數的標識(fail-fast機制)

transient關鍵字對於不想進⾏序列化的欄位,使⽤ transient 關鍵字修飾

JDK7中,只要建立ArrayList陣列,就會預設建立一個長度為10的空陣列。

JDK8中,做了一個延遲載入,在建立ArrayList陣列時,建立一個長度為0的空陣列,只有在用到這陣列才會對長度進行改變,做了一個延遲載入

建構函式

裡面包含三種建構函式

1.無參建構函式

初始化陣列時預設賦值一個空陣列

//無參建構函式
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//預設為0
    }

image-20210624173750562

2.int型別的建構函式

如果傳入數值大於0就建立指定容量大小的陣列,數值為0為空物件陣列,否則丟擲異常

//帶容量大小的建構函式
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;//空物件陣列
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

3.集合型別建構函式

把傳入的集合變成陣列,賦值給elementData

集合不為空,傳入陣列型別轉為object的話賦值給elementData

否則就直接複製到elementData中

    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();//變成陣列
        if ((size = a.length) != 0) {//集合不為空的話
            if (c.getClass() == ArrayList.class) {//判斷與ArrayList型別是否一致
                elementData = a;//賦值
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);//否則直接複製到陣列中
            }
        } else {
            elementData = EMPTY_ELEMENTDATA;//設定為空陣列
        }
    }

增加方法

add(E e)方法

 public boolean add(E e) {
        ensureCapacityInternal(size + 1); //當size=0時
        elementData[size++] = e;//Size還是為0,給為0的size賦值
        return true;
    }
  //確保集合陣列內部容量
 private void ensureCapacityInternal(int minCapacity) {//1
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));//返回10
    }
  //確保顯式容量
  private void ensureExplicitCapacity(int minCapacity) {//minCapacity=10
        modCount++;
        if (minCapacity - elementData.length > 0)// 10-0>0 判斷擴容關鍵程式碼 當第11個陣列加入時執行grow程式碼
            grow(minCapacity);//10
    }

 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

 //計算陣列容量
   private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//0=0
            return Math.max(DEFAULT_CAPACITY, minCapacity);// DEFAULT_CAPACITY=10,minCapacity=1  返回最大值作為陣列容量
        }
        return minCapacity;
    }

//擴容方法
private void grow(int minCapacity) {
        int oldCapacity = elementData.length;//0
        int newCapacity = oldCapacity + (oldCapacity >> 1); //陣列長度加上位移運算(陣列長度的一半)每次擴容增加1.5倍  0=0+0
        if (newCapacity - minCapacity < 0)//0-10<0
            newCapacity = minCapacity; //newCapacity=0
        if (newCapacity - MAX_ARRAY_SIZE > 0)//10-MAX_ARRAY_SIZE<0
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);//複製操作 把一個長度為10複製到空的陣列中,生成了一個新的長度為10的空陣列
    }

Size預設是0,add方法給Size加上1,並傳入ensureCapacityInternal方法,其中ensureCapacityInternal方法中又呼叫了ensureExplicitCapacity方法,並傳入了兩個引數,第一個是elementData代表一個為空的陣列,第二個是陣列個數。在calculateCapacity方法中,有個if判斷如果陣列大小等於預設大小,就返回其中最大的數值,預設陣列大小為10的話,DEFAULT_CAPACITY也等於10,所以返回的是10。把10傳入ensureExplicitCapacity方法中,再把10 傳入grow方法中,生成了一個新的長度為10的空陣列。ensureCapacityInternal方法執行完畢。add方法Size依然為0,給為下標為0索引賦值,新增一個成功。

擴容模擬

當新增第11個元素時,add方法Size=10+1=11,傳入ensureCapacityInternal方法,進入calculateCapacity方法,這時elementData=10,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA=0,不滿足直接返回minCapacity=11,值11進入ensureExplicitCapacity方法內部,滿足11-10>0進grow方法,grow方法賦值oldCapacity=10,newCapacity=10+10的位移一位操作=10+5=15,最後使用copyOf方法,把原來10大小的陣列擴容到15。

add(int index, E element)方法

public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1); //集合增加容量
    //elementData:原陣列  index:從原陣列這個索引開始複製  elementData:目標陣列   index + 1:目標陣列的第一個元素  size - index:複製size-index個元素
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;//把當前所有陣列替換
    size++;//索引增加
}
//判斷索引是否越界
private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

這樣每增加一個元素就要把當前索引複製到該元素的後面,開銷很大

刪除方法

remove(int index)方法

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; //集合數量減1

    return oldValue;
}

E elementData(int index) {
      return (E) elementData[index];//返回當前索引的元素
}

進行刪除前檢查索引是否合理,然後記錄集合被修改的次數,根據索引獲取到需要刪除的這個元素,然後該索引後面的元素複製到當前索引,進行覆蓋,最後集合元素減去。

remove(Object o)方法

public boolean remove(Object o) {
    if (o == null) {//判斷物件是否為空
        for (int index = 0; index < size; index++)//遍歷整個集合
            if (elementData[index] == 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; // 集合數量減1
}

該方法利用集合遍歷找到該元素,根據索引再進行刪除,fastRemove方法同remove(int index)方法類似。

removeAll(Collection<?> c)方法

刪除c集合中存在的元素


public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);//判斷集合是否為空
        return batchRemove(c, false);
}

public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
}

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)
            if (c.contains(elementData[r]) == complement)//complement=flase  如果elementData不包含c裡面的某個元素
                elementData[w++] = elementData[r];//不包含的記錄下來! 賦值到elementData,滿足就從索引0開始覆蓋掉原來的陣列
    } finally {
        //存在併發修改上面發生異常沒遍歷完elementData,這裡就會不相等   如果不存在這裡就會直接跳過
        if (r != size) {
  //elementData:原陣列  r:從原陣列這個索引開始複製  elementData:目標陣列 w:目標陣列的第一個元素  size - r:複製元素數量 
            
            System.arraycopy(elementData, r,elementData, w,size - r);//把沒有遍歷到的元素賦值到已更新元素的後面
            w += size - r;//現有的資料
        }
        if (w != size) {//w容量是不包含的  除非集合裡刪除一個元素都不包含這裡才會跳過 
            for (int i = w; i < size; i++)
                elementData[i] = null;//把不包含的後面多餘的陣列置為空
            modCount += size - w;//增加修改次數
            size = w;//當前資料長度
            modified = true;//修改成功
        }
    }
    return modified;
}

場景模擬代入數值:在一個10長度的陣列a中刪除一個3長度的陣列b,刪除到一半的時候,由於多執行緒導致陣列賦值失敗進入finally程式碼塊,此時w=3,r=6,size=10,w存入的是不包含在b陣列中的元素現有3個,r是遍歷到中斷的索引,進入(r != size)判斷不相等,arraycopy方法把後面沒有遍歷到的重新加進來陣列,w+=size - r=7;這樣w中儲存的就是c集合中不包含的全部元素,再遍歷整個Size集合,把不包含元素後面的容量置為空。

其他方法

indexOf(Object o)方法

獲取索引

public int indexOf(Object o) {
    if (o == null) {//判斷物件是否為空
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))//判斷相等
                return i;
    }
    return -1;//未匹配
}

lastIndexOf(Object o)

倒序獲取索引

public int lastIndexOf(Object o) {
    if (o == null) {//等於空
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))//判斷相等
                return i;
    }
    return -1;//未匹配
}

set(int index, E element)方法

public E set(int index, E element) {
    rangeCheck(index);//檢查索引

    E oldValue = elementData(index);//根據索引獲取元素
    elementData[index] = element;//把元素賦值給當前索引
    return oldValue;
}

get(int index)方法

public E get(int index) {
    rangeCheck(index);//檢查索引

    return elementData(index);//返回根據索引獲取當前元素
}

clear方法

public void clear() {
    modCount++;//修改次數+1

    for (int i = 0; i < size; i++)
        elementData[i] = null;//遍歷賦值為null

    size = 0;//陣列中元素的個數置為0
}

總結

  • 是一個動態陣列,其容量能自動增長,但每次增刪都要copy,效能不高,訪問的時候通過索引是最快的
  • ArrayList執行緒不安全,只能用在單執行緒環境下,多執行緒環境下可以考慮用Collections.synchronizedList(List l)函式返回一個執行緒安全的ArrayList類,也可以使用concurrent併發包下的CopyOnWriteArrayList類

相關文章