1. 程式人生 > >java集合原始碼分析(三)--ArrayList原始碼

java集合原始碼分析(三)--ArrayList原始碼

吐槽

週末啊,冷啊啊啊啊,然後怕自己週六中午睡起來都晚上了,就不睡午覺了去實驗室看下ArrayList的原始碼。
之前自己學集合只是簡單的看了下用法,寫專案的時候雖然用這塊但是也沒仔細看下這塊到底咋實現的。

ArrayList的基本功能

首先這個貨是個陣列
陣列就是存放東西的一個倉庫
但是這個和普通的普通的陣列還是有區別的
它的特殊的地方就是可以動態的新增或者減少這個數組裡面的元素emmmmm//當然也是有限制的不可能無限放東西

我們今天要看的就是ArrayList的幾個問題

  • Arraylist動態新增或者減少怎麼樣實現的?
  • 它的執行緒的安全性?
  • 它的最大的容量是多少?
  • java中Array和ArrayList區別?

ArrayList類的介紹

首先去文件上看下這塊的繼承關係
在這裡插入圖片描述
在這裡插入圖片描述
發現這個貨繼承了AbstractList然後實現了四個個介面List, RandomAccess, Cloneable, Serializable
根據我們看他繼承的類和試下的介面看到他有一下的能力

  • ArrayList 繼承了AbstractList,實現了List。它是一個數組佇列,提供了相關的新增、刪除、修改、遍歷等功能
  • ArrayList 實現了RandmoAccess介面,即提供了隨機訪問功能。RandmoAccess是java中用來被List實現,為List提供快速訪問功能的。
  • ArrayList 實現了Cloneable介面,即覆蓋了函式clone(),能被克隆。
  • ArrayList 實現java.io.Serializable介面,這意味著ArrayList支援序列化,能通過序列化去傳輸。

我們還是去看下這塊的原始碼吧233也不是很長,我們一部分一部分看

類的成員變數介紹

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
    //可序列化的版本號
    private static final long serialVersionUID = 8683452581122892189
L; //預設的陣列大小為10 重點 private static final int DEFAULT_CAPACITY = 10; //例項化一個空的陣列 當用戶指定的ArrayList為0的時候 返回這個 private static final Object[] EMPTY_ELEMENTDATA = new Object[0]; //一個空陣列例項 當用戶沒有指定 ArrayList 的容量時(即呼叫無參建構函式),返回的是該陣列==>剛建立一個 ArrayList 時,其內資料量為 0。 //當用戶第一次新增元素時,該陣列將會擴容,變成預設容量為 10(DEFAULT_CAPACITY) 的一個數組===>通過 ensureCapacityInternal() 實現 //它與 EMPTY_ELEMENTDATA 的區別就是:該陣列是預設返回的,而後者是在使用者指定容量為 0 時返回 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0]; //存放List元素的陣列 儲存了新增到ArrayList中的元素。實際上,elementData是個動態陣列 ArrayList基於陣列實現,用該陣列儲存資料, ArrayList 的容量就是該陣列的長度 //該值為 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 時,當第一次新增元素進入 ArrayList 中時,陣列將擴容值 DEFAULT_CAPACITY(10) transient Object[] elementData; //List中元素的數量 存放List元素的陣列長度可能相等,也可能不相等 private int size; //這個數字就是最大存放的大小emmmmmm private static final int MAX_ARRAY_SIZE = 2147483639;

裡面也蠻清楚的有兩個總要的物件:
elementData 陣列 "Object[]型別的陣列,後面的初始化,其他方面有很重要的用處
size 這個是動態陣列的實際大小

構造方法

有三個

public ArrayList(int var1) {
    if (var1 > 0) {
    //建立一樣大的elementData陣列
        this.elementData = new Object[var1];
    } else {
        if (var1 != 0) {
        //傳入的引數為負數時候 報錯
            throw new IllegalArgumentException("Illegal Capacity: " + var1);
        }
        //初始化這個為空的陣列
        this.elementData = EMPTY_ELEMENTDATA;
    }

}
//構造方法 無參 陣列緩衝區 elementData = {}, 長度為 0
//當元素第一次被加入時,擴容至預設容量 10
public ArrayList() {
      //初始化這個為空的陣列
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//構造方法,引數為集合元素
public ArrayList(Collection<? extends E> var1) {
//將集合元素轉換為陣列,然後給elementData陣列
    this.elementData = var1.toArray();
    if ((this.size = this.elementData.length) != 0) {
    //如果不是object型別的陣列,轉換成object型別的陣列
        if (this.elementData.getClass() != Object[].class) {
            this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
        }
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }

}

構造方法裡面這塊就發現elementData陣列真的是作用很大啊
我們發現這塊它被修飾的時候是用transient修飾的

transient是幹嘛的啊

當物件被序列化時(寫入位元組序列到目標檔案)時,transient阻止例項中那些用此關鍵字宣告的變數持久化;當物件被反序列化時(從原始檔讀取位元組序列進行重構),這樣的例項變數值不會被持久化和恢復。例如,當反序列化物件——資料流(例如,檔案)可能不存在時,原因是你的物件中存在型別為java.io.InputStream的變數,序列化時這些變數引用的輸入流無法被開啟。

那麼為什麼ArrayList裡面的elementData為什麼要用transient來修飾?

因為ArrayList不能序列化和反序列化嗎?肯定不是,是因為elementData裡面不是所有的元素都有資料,因為容量的問題,elementData裡面有一些元素是空的,這種是沒有必要序列化的。ArrayList的序列化和反序列化依賴writeObject和readObject方法來實現。可以避免序列化空的元素。
序列化的

存放元素和改變容量的方法

//改變陣列的長度,使長度和List的size相等。
//集合中元素個數的size和表示集合容量的elementData.length可能不同,在不太需要增加//集合元素的情況下容量有浪費,可以使用trimToSize方法減小elementData的大小
public void trimToSize() {
    ++this.modCount;//繼承自AbstractList中的欄位,表示陣列修改的次數,陣列每修改一次,就要增加modCount
    if (this.size < this.elementData.length) {
        this.elementData = this.size == 0 ? EMPTY_ELEMENTDATA : Arrays.copyOf(this.elementData, this.size);
    }

}
//確定ArrayList的容量
//判斷當前elementData是否是EMPTY_ELEMENTDATA,若是設定長度為10
public void ensureCapacity(int var1) {
    int var2 = this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA ? 0 : 10;
    if (var1 > var2) {
        //需要擴充
        this.ensureExplicitCapacity(var1);
    }

}
//最小擴充容量,預設是 10
//如果elementData為空的時候 看下長度和10比較的結果,找最大的
//判斷是不是空的ArrayList,如果是的最小擴充容量10,否則最小擴充量為0
//上面無參建構函式建立後,當元素第一次被加入時,擴容至預設容量 10,就是靠這句程式碼
private void ensureCapacityInternal(int var1) {
  // 若使用者指定的最小容量 > 最小擴充容量,則以使用者指定的為準,否則還是 10
    if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        var1 = Math.max(10, var1);
    }
    //擴容
    this.ensureExplicitCapacity(var1);
}
//minCapacity和預設大小(10)比較,如果需要擴大容量,繼續呼叫
private void ensureExplicitCapacity(int var1) {
    ++this.modCount;
    if (var1 - this.elementData.length > 0) {
        this.grow(var1);
    }

}
//擴容的操作
private void grow(int var1) {
 // 防止溢位程式碼
    int var2 = this.elementData.length;
    //容量擴容為之前的1.5倍
    int var3 = var2 + (var2 >> 1);
    //對新的容量進行判斷
    // 若 newCapacity 依舊小於 minCapacity
    if (var3 - var1 < 0) {
        var3 = var1;
    }
    // 若 newCapacity 大於最大儲存容量,則進行大容量分配
    if (var3 - 2147483639 > 0) {
        var3 = hugeCapacity(var1);
    }
    //複製舊元素到新的陣列上面
    this.elementData = Arrays.copyOf(this.elementData, var3);
}
//這個就是判斷大小的
private static int hugeCapacity(int var0) {
    if (var0 < 0) {
    //如果傳入的小於0的話 報錯
        throw new OutOfMemoryError();
    } else {
    //大於0的話  如果大於 ((2^31)-1) = 2147483647-8 =  2147483639
        return var0 > 2147483639 ? 2147483647 : 2147483639;
    }
}

我們看到這塊的程式碼
擴容的時候利用位運算把容量擴充到之前的1.5倍
比如說用預設的構造方法,初始容量被設定為10。當ArrayList中的元素超過10個以後,會重新分配記憶體空間,使陣列的大小增長到16。

可以通過除錯看到動態增長的數量變化:10->16->25->38->58->88->…

將ArrayList的預設容量設定為4。當ArrayList中的元素超過4個以後,會重新分配記憶體空間,使陣列的大小增長到7。

可以通過除錯看到動態增長的數量變化:4->7->11->17->26->…

公式就是 新的容量 = (舊的容量*3)/2 +1;

然後還要檢測下大小,不能超出最大的範圍

那那那這個Java中ArrayList最大容量是多少啊?
大約是8G
看下原始碼發現是2147483639 這個數字emmmmmmmmm
看下別人的原始碼是private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;好像是我看的原始碼版本問題

Integer.MAX_VALUE 是 2的31次方減一
為什麼要減8,,,原因好像是因為只是為了避免一些機器記憶體溢位, -8 是為了減少出錯的機率,虛擬機器在陣列中保留了一些頭資訊。避免記憶體溢位。

增加

//這個是 不指定位置的話 新增到陣列末尾
public boolean add(E var1) {
    this.ensureCapacityInternal(this.size + 1);
    this.elementData[this.size++] = var1;
    return true;
}
//指定位置的情況  
public void add(int var1, E var2) {
    //檢驗下是否插入的位置在陣列容量範圍內
    this.rangeCheckForAdd(var1);
    //檢查是否需要擴容
    this.ensureCapacityInternal(this.size + 1);
    System.arraycopy(this.elementData, var1, this.elementData, var1 + 1, this.size - var1);
    //騰出新空間新增元素
    this.elementData[var1] = var2;
    //修改陣列內元素的數量
    ++this.size;
}
private void rangeCheckForAdd(int var1) {
    if (var1 > this.size || var1 < 0) {
        throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1));
    }
}
//檢驗下是否插入的位置在陣列容量範圍內
private void rangeCheckForAdd(int var1) {
    if (var1 > this.size || var1 < 0) {
        throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1));
    }
}
//檢測是否要擴容
private void ensureCapacityInternal(int var1) {
    if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        var1 = Math.max(10, var1);
    }

    this.ensureExplicitCapacity(var1);
}

private void ensureExplicitCapacity(int var1) {
    ++this.modCount;
    if (var1 - this.elementData.length > 0) {
        this.grow(var1);
    }

}

查詢

它這個有兩個查詢
普通查詢
逆序查詢 思路也蠻清楚的

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;
}
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;
}

刪除

這個和陣列的刪除類似的emmmmmmmmm
都是把指定位置的元素刪除後,它後面的元素統一向前移動一位

public E remove(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    modCount++;
    E oldValue = (E) 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 work

    return oldValue;
}

 //移除指定的第一個元素
// 如果list中不包含這個元素,這個list不會改變
// 如果包含這個元素,index 之後的所有元素依次左移一位

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;
}
//快速刪除下標第index的元素
private void fastRemove(int index) {
        modCount++;//這個地方改變了modCount的值了
        int numMoved = size - index - 1;//移動的個數
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                    numMoved);
        elementData[--size] = null; //將最後一個元素清除
    }

ArrayList執行緒不安全的原因

雖然之前一直背的是ArrayList的執行緒不安全,但是還是不知道是為什麼,作業系統課上對這塊的理解就是當兩個執行緒請求資料的時候,可能一個執行緒不小心把另一個執行緒的裡面的東西改了,這塊又和計算機的儲存有關了
一般來說,,,遇到 變數++這種的,,,很容易遇到執行緒不安全的問題

我們來看下這塊的執行緒不安全的原因
在之前的成員變數裡面我們說過裡面有兩個貨特別重要
transient Object[] elementData;
private int size;

elementData是個動態陣列 ArrayList基於陣列實現,用該陣列儲存資料, ArrayList 的容量就是該陣列的長度
個size變數用來儲存當前陣列中已經添加了多少元素

外面再進行add()操作時候的一系列原始碼

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(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;
    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);
}

之前上面分析了這幾個函數了的 這塊就不講了
主體思路就是我要新增一個元素,先進行判斷一下,看下里面空間夠不夠,不夠的話我就擴容什麼的,然後再把這個元素加進去。裡面其實就是兩部操作
1判斷elementData陣列容量是否滿足需求
2在elementData對應位置上設定值

所以,這塊就是出現執行緒不安全的第一個地方,在多個執行緒進行add操作時可能會導致elementData陣列越界。具體邏輯如下:

  1. 列表大小為9,即size=9
  2. 執行緒A開始進入add方法,這時它獲取到size的值為9,呼叫ensureCapacityInternal方法進行容量判斷。 執行緒A開始進入add方法,這時它獲取到size的值為9,呼叫ensureCapacityInternal方法進行容量判斷
  3. 執行緒B此時也進入add方法,它獲取到size的值也為9,也開始呼叫ensureCapacityInternal方法。 執行緒B此時也進入add方法,它獲取到size的值也為9,也開始呼叫ensureCapacityInternal方法
  4. 執行緒A發現需求大小為10,而elementData的大小就為10,可以容納。於是它不再擴容,返回。執行緒A發現需求大小為10,而elementData的大小就為10,可以容納。於是它不再擴容,返回
  5. 執行緒B也發現需求大小為10,也可以容納,返回。 執行緒A開始進行設定值操作, elementData[size++] = e執行緒B也發現需求大小為10,也可以容納,返回
  6. 執行緒A開始進行設定值操作, elementData[size++] = e執行緒A開始進行設定值操作, elementData[size++] = e操作。此時size變為10。
  7. 執行緒B也開始進行設定值操作,它嘗試設定elementData[10] =執行緒B也開始進行設定值操作,它嘗試設定elementData[10] =e,而elementData沒有進行過擴容,它的下標最大為9。於是此時會報出一個數組越界的異常ArrayIndexOutOfBoundsException.

然後第二個執行緒不安全的地方就是 elementData[size++] = e 設定值的操作同樣會導致執行緒不安全。這塊不是原子操作,所以會髒資料,他由兩步操作構成
1elementData[size] = e;
2size = size + 1;

所以這塊也就是在多執行緒的時候執行的話出現問題了

  1. 列表大小為0,即size=0
  2. 執行緒A開始新增一個元素,值為A。此時它執行第一條操作,將A放在了elementData下標為0的位置上。執行緒A開始新增一個元素,值為A。此時它執行第一條操作,將A放在了elementData下標為0的位置上。
  3. 接著執行緒B剛好也要開始新增一個值為B的元素,且走到了第一步操作。此時執行緒B獲取到size的值依然為0,於是它將B也放在了elementData下標為0的位置上。
  4. 接著執行緒B剛好也要開始新增一個值為B的元素,且走到了第一步操作。此時執行緒B獲取到size的值依然為0,於是它將B也放在了elementData下標為0的位置上。接著執行緒B剛好也要開始新增一個值為B的元素,且走到了第一步操作。此時執行緒B獲取到size的值依然為0,於是它將B也放在了elementData下標為0的位置上。
  5. 執行緒A開始將size的值增加為1執行緒A開始將size的值增加為1
  6. 執行緒B開始將size的值增加為2執行緒B開始將size的值增加為2
    這樣執行緒AB執行完畢後,理想中情況為size為2,elementData下標0的位置為A,下標1的位置為B。而實際情況變成了size為2,elementData下標為0的位置變成了B,下標1的位置上什麼都沒有。並且後續除非使用set方法修改此位置的值,否則將一直為null,因為size為2,新增元素時會從下標為2的位置上開始。

如何使ArrayList執行緒安全

  • 繼承Arraylist,然後重寫或按需求編寫自己的方法,這些方法要寫成synchronized,在這些synchronized的方法中呼叫ArrayList的方法。
  • List list = Collections.synchronizedList(new ArrayList());

當多執行緒訪問這些容器類時,可能會出現資料同步導致的問題,java的工具類java.util.Collections提供了將非同步物件轉換為同步物件的方法
當多執行緒訪問這些容器類時,可能會出現資料同步導致的問題,java的工具類java.util.Collections提供了將非同步物件轉換為同步物件的方法

自己對ArrayList的語言總結

因為昨天學長給我們模擬面試,然後我發現昨天早上才把原始碼看了,但是自己還是講東西給面試官還是很卡頓。所以我覺的把每次學的東西自己組織語言過一遍,然後再給別人講就會好一點。

問:簡單介紹下ArrayList
答:ArrayList是以陣列實現,可以自動擴容的動態陣列,當超出限制的時候會增加50%的容量,用ystem.arraycopy()複製到新的陣列,因此最好能給出陣列大小的預估值。預設第一次插入元素時建立大小為10的陣列。arrayList的效能很高效,不論是查詢和取值很迅速,但是插入和刪除效能較差,該集合執行緒不安全。

問:ArrayList的自動擴容怎麼樣實現的
關鍵字:elementData size ensureCapacityInternal
答:每次在add()一個元素時,arraylist都需要對這個list的容量進行一個判斷。如果容量夠,直接新增,否則需要進行擴容。在1.8 arraylist這個類中,擴容呼叫的是grow()方法。
在核心grow方法裡面,首先獲取陣列原來的長度,然後新增加容量為之前的1.5倍。隨後,如果新容量還是不滿足需求量,直接把新容量改為需求量,然後再進行最大化判斷。
通過grow()方法中呼叫的Arrays.copyof()方法進行對原陣列的複製,在通過呼叫System.arraycopy()方法進行復制,達到擴容的目的。

問:ArrayList的構造方法過程答:ArrayList裡面有三種構造方法,第一種:無參的構造方法 先將陣列為空,第一次加入的時候 然後擴充預設為10, 第二種是有參的構造方法 ,直接建立這個陣列 第三種是傳入集合元素,先將集合元素轉換為陣列,把不是object的陣列轉化為object陣列。

問:ArrayList可以無限擴大嗎?答:不能,大於是8G,因為在ArrayList擴容的時候,有個界限判斷。 private static final int MAX_ARRAY_SIZE = 2147483639,2的31次方減一然後減8,-8 是為了減少出錯的機率,虛擬機器在陣列中保留了一些頭資訊。避免記憶體溢位。