JAVA集合系列(3):ArrayList源碼剝絲抽繭,擴容原理深度解析

目錄

前言

1、List集合和它的方法們

1.1 舉個栗子

1.2 測試結果

2、List接口的實現類們

2.1 ArrayList

2.2 ArrayList源碼解讀

2.3 ArrayList源碼分析

2.3.1 ArrayList的構造方法

2.3.2 ArrayList的擴容機制

2.3.3 ArrayList擴容的核心方法

2.3.4 舉個栗子:給一個空數組添加11個元素,查看數組擴容情況

2.4數組複製

2.4.1 Arrays.copyOf()方法

2.4.2 Arrays.copyOf()栗子

2.4.3 System.arraycopy()方法

2.4.4 System.arraycopy()栗子

2.4.5 copyOf()和arraycopy()聯繫與區別

3、總結


前言

上一節內容,我們分析總結了集合的頂層的Collection和Iterator接口,下圖可以清晰的看到集合的各個接口和實現類。

自上而下,逐步分析原則,本節主要分析總結一下Collection接口的子類接口List集合,並對List集合的一個實現類ArrayList源碼進行深度解析。


1、List集合和它的方法們

List集合代表一個元素有序、可重複的集合。集合中的每個元素都有對應的順序索引。List集合允許使用重複元素,可存放多個null元素,有的元素是以一種線性方式進行存儲的,在過程中可以通過索引來訪問集合中的指定元素,可以通過索引來訪問指定位置的集合元素,且默認按照元素的添加順序設置元素的索引。

List作爲Collection接口的子接口,當然可以使用Collection接口裏的全部方法。而且由於List是有序集合,因此List集合裏增加了一些根據索引來操作集合元素的方法。這裏還是總結下List集合常用的一些方法:

方法 描述
添加元素
void add(int index, Object element) 將元素element插入到List集合的index處
boolean addAll(int index, Collection col) 將集合col所包含的所有元素都插入到List集合的index處
獲取功能
Object get(int index) 返回集合index處的元素
Iterator iterator() 用來獲取集合中每一個元素。
索引操作
int indexOf(Object obj) 返回對象obj在List集合中第一次出現的位置索引
int lastIndexOf(Object obj) 返回對象obj在List集合中最後一次出現的位置索引
設值功能
Object set(int index, Object element) 將index索引處的元素替換成element對象,返回被替換的舊元素
元素操作
List subList(int fromIndex, int toIndex) 返回從索引fromIndex(包含)到索引toIndex(不包含)處的所有集合元素組成的子集合
void replaceAll(UnartOperator operator) 根據operator指定的計算規則重新設置List集合的所有元素
排序功能
void sort(Comparator c) 根據Comparator參數對List集合的元素排序
刪除功能
void clear() 移除集合中的所有元素
boolean remove(Object o) 從集合中移除指定的元素
boolean removeAll(Collection<?> c) 從集合中移除一個指定的集合元素(有一個就返回true)
Object remove(int index) 根據索引刪除元素,返回被刪除的元素
集合轉數組
Object[] toArray() 集合轉換爲數組
判斷功能
boolean isEmpty() 判斷集合是否爲空
boolean contains(Object o) 判斷集合中是否包含指定元素
boolean containsAll(Collection<?> c) 判斷集合中是否包含指定的一個集合中的元素
列表迭代器
ListIterator listIterator() List集合特有的迭代器

1.1 舉個栗子

針對上面的方法,這裏舉個栗子運行測試:

/**
 * E set(int index,E);
 * 修改指定索引上的元素
 * 返回被修改的元素
 * <p>
 * add(int index,Object obj)
 * 將元素插入到list集合指定索引index處,
 * 注意:帶有索引的操作,防止索引越界
 * java.lang.IndexOutOfBoundsException
 * java.lang.ArrayIndexOutOfBoundsException
 * java.lang.StringIndexOutOfBoundsException
 */
@Test
public void test_set() {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(4);
    list.add(4);
    System.out.println("[0]原始的集合:" + list);

    //[1]修改指定索引上的元素
    Integer a = list.set(0, 5);
    System.out.println("[1]修改後的集合:" + list);
    //[2]返回被修改的元素
    System.out.println("[2]返回被修改的元素:" + a);

    //[3]獲取指定索引上的元素
    Integer value = list.get(0);
    System.out.println("[3]獲取指定索引上的元素:" + value);

    //[4]移除指定索引位置上的元素
    Integer removeValue = list.remove(0);
    System.out.println("[4]返回刪除指定索引上的元素:" + removeValue);

    //[5]將新元素插入到List集合中指定索引index處
    list.add(0, 123456);
    System.out.println("[5]新增指定索引元素後的集合:" + list);

    //[6]判斷集合是否爲空
    boolean isEmpty = list.isEmpty();
    System.out.println("[6]判斷集合是否爲空:" + list);

    //[7]獲取集合長度/元素個數
    int size = list.size();
    System.out.println("[7]集合的長度/元素個數:" + size);

    //[8]將集合轉爲數組
    Object[] objects = list.toArray();
    for (Object values : objects) {
        System.out.println("[8]將集合轉換爲數組,遍歷集合元素:" + values);
    }

    //[9]判斷集合是否包含某個元素
    boolean contains = list.contains("456");
    System.out.println("[9]判斷集合是否包含指定元素:" + contains);

    //[10]遍歷當前集合
    for (int value2:list) {
        System.out.println("[10]遍歷當前集合的元素:" + value2);
    }
}

1.2 測試結果

[0]原始的集合:[1, 2, 3, 4, 4]
[1]修改後的集合:[5, 2, 3, 4, 4]
[2]返回被修改的元素:1
[3]獲取指定索引上的元素:5
[4]返回刪除指定索引上的元素:5
[5]新增指定索引元素後的集合:[123456, 2, 3, 4, 4]
[6]判斷集合是否爲空:[123456, 2, 3, 4, 4]
[7]集合的長度/元素個數:5
[8]將集合轉換爲數組,遍歷集合元素:123456
[8]將集合轉換爲數組,遍歷集合元素:2
[8]將集合轉換爲數組,遍歷集合元素:3
[8]將集合轉換爲數組,遍歷集合元素:4
[8]將集合轉換爲數組,遍歷集合元素:4
[9]判斷集合是否包含指定元素:false
[10]遍歷當前集合的元素:123456
[10]遍歷當前集合的元素:2
[10]遍歷當前集合的元素:3
[10]遍歷當前集合的元素:4
[10]遍歷當前集合的元素:4

2、List接口的實現類們

在前言集合結構圖中,我們可以清楚看到List接口的兩個重要的實現類:ArrayListLinkedList

2.1 ArrayList

ArrayList是List接口的實現類,它是一個數組隊列(動態數組)。與Java中的數組相比,它的容量能動態增長。在 Java 1.2 引入了強大豐富的 Collection 框架,其中用 ArrayList 來作爲可動態擴容數組的列表實現來代替 Array 在日常開發的使用,ArrayList 實現所有列表的操作方法,方便開發者操作列表集合。

爲了更好地認識 ArrayList,看下從 ArrayList 的UML類圖:

ArrayList繼承了AbstractList類,實現了List、 Cloneable、RandomAccess、java.io.Serializable這些接口。

AbstractList 作爲列表的抽象實現,將元素的增刪改查都交給了具體的子類去實現,在元素的迭代遍歷的操作上提供了默認實現。

  • 實現Cloneable接口:表示了ArrayList 支持調用 Object 的 clone ()方法,實現 ArrayList 的拷貝。
  • 實現Serializable接口:說明 ArrayList 還支持序列化和反序列操作,具有固定的 serialVersionUID 屬性值,通過序列化輸出。
  • 實現RandomAccess接口:表示 ArrayList 裏的元素可以被高效效率的隨機訪問,以索引下標數字的方式獲取元素。實現 RandomAccess 接口的列表上在遍歷時可直接使用普通的for循環方式,並且執行效率上給迭代器方式更高。

2.2 ArrayList源碼解讀

進入正題,對ArrayList源碼逐一註釋解讀:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默認初始化容量:10
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 用於空實例的共享數組實例
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用於默認大小空實例的共享空數組實例。
     * 把它從EMPTY_ELEMENTDATA數組中區分出來,以知道在添加第一個元素時容量需要增加多少。
     * 實際上,這個空數組默認爲0,增加第一個元素時數組容量將變爲10
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 保存ArrayList數據的數組
     */
    transient Object[] elementData;

    /**
     * ArrayList數組的長度(包含的元素個數)
     */
    private int size;

    /**
     * 構造一個指定容量大小的空數組
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
	    //構造一個容量爲initialCapacity的空數組
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
	    //構造一個空的數組
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * 構造一個初始容量爲10的空List集合。默認爲0,初始化爲10,即增加第一個元素時數組容量將變爲10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 構造一個包含指定集合元素的List集合
     * 按照指定集合迭代器返回元素的順序排序
     */
    public ArrayList(Collection<? extends E> c) {
	//將集合轉爲數組
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
	    //返回的數組可能不是Object[]
	    //反射方法的getClass()
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 用空數組代替
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

    /**
     * 修改這個ArrayList實例的容量以達到當前列表集合的長度
     * 應用程序可以操作最小化存儲的ArrayList實例
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

    /**
     * ===================> ArrayList擴容問題 <===================
     *
     * 如有必要,增加此ArrayList實例的容量,以確保它至少能容納元素的數量
     * @param   minCapacity   所需最小容量
     */
    public void ensureCapacity(int minCapacity) {
	//判斷保存ArrayList數據的數組長度與初始容量爲10的空List集合(默認爲10,初始化時爲10)長度的對比
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // 默認初始化容量:10
            : DEFAULT_CAPACITY;
	    //如果最小容量 > 最小擴容大小
        if (minCapacity > minExpand) {
	    //判斷是否需要擴容,以取到最小的擴容量
            ensureExplicitCapacity(minCapacity);
        }
    }
	
    //得到最小的擴容量,用在add新增元素方法中。
    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方法進行擴容,此處表示已經開始擴容
            grow(minCapacity);
    }

    /**
     * 要分配的最大數組的長度:2147483639
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * ===================> ArrayList擴容的核心方法:grow()<===================
     * 
	 * 擴大容量,以確保它能夠裝填被最小容量參數所指定的最少元素個數
     * @param minCapacity 所需的最小容量
     */
    private void grow(int minCapacity) {
        // oldCapacity:舊容量
	// newCapacity:新容量
        int oldCapacity = elementData.length;
	//擴容,將舊容量右移一位,相當於在舊容量基礎上再次擴大0.5倍,然後組成新的容量
        int newCapacity = oldCapacity + (oldCapacity >> 1);
	//判斷新容量與所需容量的大小,如果小於最小容量,取大值者
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //判斷新容量與最大數組長度的大小,如果大於最大數組長度,就對所需的最小容量進行最大容量值檢測
	//所需的最小容量<0時,內存異常,將交給GC處理
	//如果所需的最小容量>最大數組容量,則取容量最大值作爲新的容量
	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);
    }
	
    //比較minCapacity 和 MAX_ARRAY_SIZE
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

    /**
     * 返回List列表元素個數
     */
    public int size() {
        return size;
    }

    /**
     * 判斷集合是否爲空,如果空,返回true;反之,返回false
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 判斷集合是否包含指定對象
     * 
     */
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

    /**
     * 返回指定對象第一次出現的索引;如果元素爲空或不存在則返回-1
     */
    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;
    }

    /**
     * 克隆模式
     */
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

    /**
     * 數組轉換
     */
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

    /**
     * 數組轉換
     */
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

    // Positional Access Operations
	
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

    /**
     * 獲取指定位置索引上的元素
     */
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    /**
     * 給指定位置索引元素賦值,替換原來的元素
     * 返回被替代的原始元素
     */
    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

    /**
     * 重寫collection接口方法,新增元素,
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    /**
     * ArrayList自己的方法,在指定位置索引出插入元素element
     */
    public void add(int index, E element) {
	//校驗新增元素指定索引值,如果指定的index值>數組最大長度,或者index值<0,拋索引越界異常
        rangeCheckForAdd(index);
		
	//取最小容量
        ensureCapacityInternal(size + 1);  // Increments modCount!!
	//數組複製,實際上在index處將數組分割爲兩段,然後將index後半段數組往後移動一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
	//然後將element元素放在index索引位置處
        elementData[index] = element;
	//完成元素插入操作後,數組總長度+1
        size++;
    }

    /**
     * 移除指定位置索引上的元素,並返回被移出的元素
     */
    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 work

        return oldValue;
    }

    /**
     * 移除對象,操作成功,返回true;操作失敗,返回false
     */
    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; // clear to let GC do its work
    }

    /**
     * 清空數組元素
     */
    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

    /**
     * 添加集合
     */
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

    /**
     * 將指定集合元素插入在在指定位置索引上
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

    /**
     * 移除指定索引範圍的元素
     */
    protected void removeRange(int fromIndex, int toIndex) {
        modCount++;
        int numMoved = size - toIndex;
        System.arraycopy(elementData, toIndex, elementData, fromIndex,
                         numMoved);

        // clear to let GC do its work
	//改變數組長度,交給GC處理,
        int newSize = size - (toIndex-fromIndex);
        for (int i = newSize; i < size; i++) {
            elementData[i] = null;
        }
		//處理完成後,賦予數組最新長度
        size = newSize;
    }

    /**
     * 對指定索引的範圍檢測,並給出越界異常信息
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    /**
     * 數組調用add方法和addAll時,將使用下面這個檢測索引是否越界的方法,並給出越界異常信息
     */
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    /**
     * 輸出索引越界提示信息
     */
    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }

2.3 ArrayList源碼分析

2.3.1 ArrayList的構造方法

進入正題,直接看ArrayList構造方法的源碼設計:

/**
 * 構造一個指定容量大小的空數組
 */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //構造一個容量爲initialCapacity的空數組
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
	    //構造一個空的數組
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * 構造一個默認容量爲0的空List集合。
     * 默認容量爲0,初始化容量爲10,即增加第一個元素時數組容量將變爲10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 構造一個包含指定集合元素的List集合
     * 按照指定集合迭代器返回元素的順序排序
     */
    public ArrayList(Collection<? extends E> c) {
	//將集合轉爲數組
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
	    //返回的數組可能不是Object[]
	    //反射方法的getClass()
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 用空數組代替
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

在上邊源碼中,可以看到ArrayList提供了三個構造方法:

  • 一個int類型參數的構造方法,指定了初始化數組容量大小,及數組元素的個數;
  • 一個無參構造方法,無參默認是空集合,當調用add()方法添加第一個元素時集合容量/長度會自動填充爲10;
  • 一個包含指定集合參數的構造方法 。

以上三個構造方法最後均對elementData進行了初始化,那這個elementData是什麼呢?看源碼中定義:

/**
 * 保存ArrayList數據的數組
 */
transient Object[] elementData;

elementData是一個保存ArrayList數據的數組,事實上,當我們操作ArrayList的時候,其本質上就是在對elementData這數組進行操作,由此,可以解開ArrayList底層是數組結構真相。


2.3.2 ArrayList的擴容機制

我們知道,當定義一個靜態數組時,需要指定數組的初始元素,並不需要去指定數組長度,由底層自動指定容量大小即可;當定義一個動態數組時,此時並不知道數組的初始元素,此時必須要指定數組容量大小,才能夠完成動態數組初始化。

接下來,給數組任意增加元素,看數組長度(容量)是如何變化的

測試程序:

@Test
public void test_add(){
    ArrayList<String> list = new ArrayList<>();
    list.add("a");
    System.out.println(list);
}

上面就是常見的數組添加元素方式,這裏調用List接口的add(E e)方法,在ArrayList類中做了實現。OK,我們看下ArrayList類中關於add(E e)這個方法是如何設計的:

/**
 * ArrayList數組的長度(容器的元素個數)
 */
private int size;

/**
 * 重寫collection接口方法,新增元素,
 */
public boolean add(E e) {
    //調用獲取最小擴容量方法
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

 增加一個元素時,調用獲取最小擴容方法,同時將原來ArrayList數組元素的個數(或者數組長度)增加1,我們進入ensureCapacityInternal(size + 1);這個方法一探究竟:

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

 在這裏,將調用數組判斷擴容的方法。同時,這裏有傳遞了一個計算數組容量的方法,實際上判斷數組擴容就是在calculateCapacity()這個方法中執行的。

接下來,剝絲抽繭,繼續看calculateCapacity()方法:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

 先看這個方法的返回值,最終返回一個最小容量的整數值,這個最小容量咋來的呢?

原來,在新元素添加到數組前,傳遞了一個用來存儲實際內容的數組(lementData),以及這個數組最新的元素個數(數組長度);這裏,我們在回顧一下DEFAULTCAPACITY_EMPTY_ELEMENTDATA:

//用於默認大小空實例的共享空數組實例。
//把它從EMPTY_ELEMENTDATA數組中區分出來,以知道在添加第一個元素時容量需要增加多少。
//實際上,這個空數組默認爲0,增加第一個元素時數組容量將變爲10
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 默認初始化容量:10
 */
private static final int DEFAULT_CAPACITY = 10;

調用數學函數二者比大小,取大值。這裏做了初始化操作,如果爲數組爲空的話(即初始空數組),則容量填充爲10 ;如果開始添加第二個元素(或者說這個數組原本是有元素的),則返回原數組長度+1的計算值,即minCapacity(最小容量)。

繼續下一步:

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

    //拿數組當前長度與原來的數組長度比大小,如果比原來容量大則進行擴容。
    if (minCapacity - elementData.length > 0)
        //擴容的核心方法,此處表示已經開始擴容
        grow(minCapacity);
}

此時,拿數組當前長度與原來的數組長度比大小,如果比原來容量大則進行擴容。

OK,上面詳細分析了數組添加元素從開始到即將觸發數組擴容機制的完整流程。一起來看下數組擴容時,核心擴容方法grow()是如何處理的。


2.3.3 ArrayList擴容的核心方法

    /**
     * 要分配的最大數組的長度:2147483639
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * ===================> ArrayList擴容的核心方法:grow()<===================
     * 
	 * 擴大容量,以確保它能夠裝填被最小容量參數所指定的最少元素個數
     * @param minCapacity 所需的最小容量
     */
    private void grow(int minCapacity) {
        // oldCapacity:舊容量
	// newCapacity:新容量
        int oldCapacity = elementData.length;
	//擴容,將舊容量右移一位,相當於在舊容量基礎上再次擴大0.5倍,然後組成新的容量,新容量是舊容量的1.5倍大小
        int newCapacity = oldCapacity + (oldCapacity >> 1);
	//判斷新容量與所需容量的大小,如果小於最小容量,取大值者
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //判斷新容量與最大數組長度的大小,如果大於最大數組長度,就對所需的最小容量進行最大容量值檢測
	//所需的最小容量<0時,內存異常,將交給GC處理
	//如果所需的最小容量>最大數組容量,則取容量最大值作爲新的容量
	if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 數組複製
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
	
    //比較minCapacity 和 MAX_ARRAY_SIZE
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

ArrayList的擴容機制提高了性能,如果每次只擴充一個(也就是每次只新增一個元素),那麼頻繁的插入會導致頻繁的數組拷貝,降低性能,而ArrayList的擴容機制避免了這種情況。

而且,ArrayList相當於在沒指定initialCapacity(初始容量)時就是會使用延遲分配對象數組空間,數組第一次插入元素時會分配容量爲10的元素空間。


2.3.4 舉個栗子:給一個空數組添加11個元素,查看數組擴容情況

測試程序:

@Test
public void test_add(){
    ArrayList list = new ArrayList();
    for (int i = 0; i < 11; i++) {
        list.add(i);
    }
}
  • 在第一次添加一個元素時,ArrayList的容量由0變爲10,此時數組成功添加了第一個元素;下圖所示:

  • 在數組添加第一個元素開始擴容,一直到數組尚未複製前這個階段,我們來DEBUG看下數組容量情況:

這裏可以清楚的看到,第一個元素添加到數組,數組舊容量還是0(因爲此時數組元素尚未完成添加操作,數組長度還是0狀態),新容量將變爲初始化容量10;然後開始數組拷貝,徹底完成空數組添加第一個元素及第一次擴容操作 。

  • 數組擴容爲10後,以此作爲舊容量去繼續添加元素,且數組新容量按照舊容量的1.5倍舊容量值進行增長。
  • 在數組增加第10個元素時,容量還是10:

  • 在輸入增加第11個元素時,容量將又一次擴容,新容量將變爲1.5*10:

  • 擴容操作後,數組將通過Arrays.copyOf(elementData, newCapacity) 這樣的方式完成數組複製,進而複製成最新數組。

2.4 數組複製問題

上面源碼,我們發現數組在經過擴容處理後,將對數組進行復制操作。

在ArrayList數組中,我們可以看到三種數組複製方法:Arrays.copyOf()、System.arraycopy()、clone()方法

clone()源碼:

實際上也是通過Arrays.copyOf(elementData, size)方法,因此,這裏我們只需要瞭解:Arrays.copyOf()、System.arraycopy()

2.4.1 Arrays.copyOf()方法

數組擴容核心方法grow()完成擴容後對數組的處理:很巧妙的用到了Arrays.copyOf()方法將複製數組至指定長度的新容量長度;

  • Arrays 類的 copyOf() 方法的語法格式:
  • Arrays.copyOf(dataType[] srcArray, int length);
  • 參數解釋:
  • srcArray 表示要進行復制的數組;
  • length 表示複製後的新數組的長度。
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:要複製的數組;newCapacity:要複製的長度
    elementData = Arrays.copyOf(elementData, newCapacity);
}

這裏,我們再進入到copyOf()方法的源碼中一探究竟:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    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;
}

原來,Arrays.copyOf()複製數組的底層還是用的System.arraycopy()方法實現的。

2.4.2 Arrays.copyOf()栗子

    @Test
    public void test_copyOf() {
        //源數組
        int[] srcArray = {1, 2, 3, 4, 5};
        System.out.print("原始數組:");
        for (int i = 0; i < srcArray.length; i++) {
            System.out.print(srcArray[i] + "\t");
        }

        //目標數組
        int[] destArray = Arrays.copyOf(srcArray, 8);
        System.out.println();
        System.out.print("目標數組:");
        for (int i = 0; i < destArray.length; i++) {
            System.out.print(destArray[i] + "\t");
        }
    }

測試結果:

原始數組:1	2	3	4	5	
目標數組:1	2	3	4	5	0	0	0	

上邊栗子,源數組長度5,元素已知,將其複製到長度爲8的目標數組中,在複製完源數組的5個元素後,還有三個索引空間如何處理,這裏將會採用數組類型的默認值填充剩餘 3 個元素的內容,即填充爲0。

使用這種方法複製數組時,默認從源數組的第一個元素(索引0)開始複製,目標數組的長度將爲 length。

  • 如果 length 大於源數組長度arr.length,則目標數組dest採用默認值填充;
  • 如果 length 小於arr.length,則複製到第 length個元素(索引值爲 length-1)即止。 
  • 注意:目標數組如果已經存在,將會被重構。

2.4.3 System.arraycopy()方法

ArrayList中實現數組複製的方法使用很巧妙,以add(int index, E element)爲例,巧妙地用到了arraycopy()方法讓數組自己複製自己實現讓索引index開始之後的所有成員後移一個位置:

//在指定索引處添加元素
public void add(int index, E element) {
    //索引範圍檢測
    rangeCheckForAdd(index);
    
    //數組容量伸縮處理
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //複製數組
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    //將新增元素放在數組中指定索引位置 ,也就是給只給索引處元素賦值
    elementData[index] = element;
    //同步更新數組的長度
    size++;
}

arraycopy() 方法位於 java.lang.System 類中,定義如下

  • 語法格式: 

System.arraycopy(dataType[] srcArray, int srcIndex, int destArray, int destIndex, int length)

  • 參數解釋:
  • srcArray 表示源數組;
  • srcIndex 表示源數組中的起始索引(包含);
  • destArray 表示目標數組;
  • destIndex 表示目標數組中的起始索引(不包含,);
  • length表示要複製的源數組長度。
  • 注意:
  • length + srcIndex的長度須 <= 源數組長度srcArray.length
  • length+destIndex的長度須 <= 目標數組長度destArray.length

2.4.4 System.arraycopy()栗子

    @Test
    public void test_arraycopy(){
        //源數組
        int[] srcArray = {1, 2, 3, 4, 5,6};
        System.out.print("原始數組:");
        for (int i = 0; i < srcArray.length; i++) {
            System.out.print(srcArray[i] + "\t");
        }
        //目標數組
        int[] destArray = {99, 98, 97, 96, 95, 94, 93, 92, 91};
        System.out.println();
        System.out.print("目標數組:");
        for (int i = 0; i < destArray.length; i++) {
            System.out.print(destArray[i] + "\t");
        }

        //複製源數組中的一部分到目標數組中
        System.out.println();
        System.arraycopy(srcArray,0,destArray, 6, 3);
        System.out.print("複製數組:");
        for (int i = 0; i < destArray.length; i++) {
            System.out.print(destArray[i] + "\t");
        }
    }

測試結果:

原始數組:1	2	3	4	5	6	
目標數組:99	98	97	96	95	94	93	92	91	
複製數組:99	98	97	96	95	94	1	2	3

 這裏定義一個長度5,元素已知的int源數組srcArray;然後,定義一個長度9,元素已知的目標數組destArray;執行數組複製System.arraycopy(srcArray,0,destArray,6,3)方法,將源數組索引0開始3個元素複製到目標數組索引開始的3個位置索引上。

2.4.5 copyOf()和arraycopy()聯繫與區別

  • 聯繫:
  • 分析源碼,Arrays.copyOf()方法內部調用了System.arraycopy()方法
  • 區別:
  • arraycopy()實現將目標數組拷貝到自己定義的目標數組中,並能夠選擇拷貝的起點和長度以及放入新數組中的位置
  • copyOf()是將源數組複製爲一個指定長度的目標新數組,並能夠返回該目標數組。

3、總結

List集合:

  • 【1】List集合代表一個元素有序、可重複的集合,集合中的每個元素都有對應的順序索引。
  • 【2】List集合允許使用重複元素,可存放多個null元素,有的元素是以一種線性方式進行存儲的。
  • 【3】可以通過索引來訪問集合中的指定元素,可以通過索引來訪問指定位置的集合元素,且默認按照元素的添加順序設置元素的索引。

ArrayList數組:

  • 【1】 ArrayList是一個保存數據的數組,ArrayList實現java.io.Serializable的方式。當寫入到輸出流時,先寫入“容量”,再依次寫入“每一個元素”;當讀出輸入流時,先讀取“容量”,再依次讀取“每一個元素”。
  • 【2】使用ArrayList的默認構造函數時,ArrayList默認容量大小:10
  • 【3】當ArrayList容量小於所需元素個數時,ArrayList將擴容:新的容量=“1.5 * 原始容量”
  • 【4】ArrayList的克隆函數,就是將全部源數組元素克隆到一個新的目標數組中。

願你就像早晨八九點鐘的太陽,活力十足,永遠年輕。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章