ArrayList源碼分析(一)

這篇文章來自個人我的博客java

正文以前

最近在複習Java基礎,感受又學到了好多,集合做爲Java的一大重點,以前是看着《Java編程思想》學的,此次恰好配合着源碼從新學一遍,首先學的是ArrayList,順便作點總結(迭代器無關):編程

  1. ArrayList的基本概念
  2. ArrayList的源碼剖析(常量,構造器,經常使用方法)

關於ArrayList的源碼,至少要寫三四篇纔夠吧,這源碼但是有足足1500行左右(一半是註釋),慢慢積累吧數組

正文

ArrayList基本概念

剛開始學的時候管它叫動態數組,由於它的大小是根據數據量而自動變化的,它的結構是基於動態數組(Dynamic array)瀏覽器

註釋

我在IDEA中之間右鍵點擊ArrayList查看了源碼,先將註釋做爲HTML放在瀏覽器中看看:bash

把它稱做Resizable-array就能夠看出它的性質了多線程

接下來講說這一整頁的大概內容:dom

  1. 它實現了List接口,實現了全部List的方法,它能夠容納全部元素類型(包括null),ArrayList除了不是多線程同步以外,其他的內容都與Vector大體相同測試

  2. size, isEmpty, get, set, iterator 和 listIterator 操做用時是常數級別的,add 操做耗時是和插入位置有關,除此以外,其餘操做的時間是線性增加的,下面會作個實驗驗證一下ui

  3. Arraylist有着容量的概念,容量隨着數據量的增加而增加,也能夠在直接定義這個容器的大小,使用ensureCapacity操做,可以一次增加所須要的容量,提升效率,避免一個個添加從而不斷增加容量this

  4. 劃重點,上面說過這個容器不是同步的,若是在多線程中使用,有一個線程要改變容器的結構時,就須要在容器外部進行同步,官方推薦的作法是用同步的容器將它包裝起來

List list = Collections.synchronizedList(new ArrayList(...));
複製代碼
  1. 註釋中接下來關於迭代器的咱們就先跳過,以後會有專門分析迭代器的源碼
Demo:

測試一下第2點中的運行時間:

  • 先算一下add操做的運行時間:
import java.util.ArrayList;
import java.util.List;

public class Test {
    public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        long startTime = 0, endTime = 0;

        //插入操做
        startTime = System.currentTimeMillis();
        //須要大一點的數據量才能算出時間
        for (int i = 0; i < 100000; i++) {
            list.add(String.valueOf(i));
        }
        endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);

        //
        startTime = System.currentTimeMillis();
        for (int i = 100000; i < 200000; i++) {
            list.add(String.valueOf(i));
        }
        endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);
        list.clear();
    }
}
複製代碼

從十萬的位置開始插入,多用了一毫秒,說明運行時間和插入位置是有關的

  • get操做
import java.util.ArrayList;
import java.util.List;

public class Test {
    public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        long startTime = 0, endTime = 0;
        String index;

        //插入操做
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 2000000; i++) {
            list.add(String.valueOf(i));
        }
        endTime = System.currentTimeMillis();
        System.out.println("插入兩百萬數據時間: ");
        System.out.println(endTime - startTime);

        startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            index = list.get(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("獲得前一百萬數據時間: ");
        System.out.println(endTime - startTime);


        startTime = System.currentTimeMillis();
        for (int i = 1000000; i < 2000000; i++) {
            index = list.get(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("獲得前一百萬數據時間: ");
        System.out.println(endTime - startTime);
        list.clear();
    }
}
複製代碼

get操做時間在相同數據量的前提下是同樣的

其餘的都是用差很少的方法來驗證,就不作了

源碼剖析

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

說明這個類是實現了幾個接口:

  • List
  • RandomAccess
  • Cloneable
  • java.io.Serializable

第一個不說,說說後面三個,這三個接口都是空的,只是用來講明:

支持快速隨機訪問


可以使用Object.clone()方法


可序列化


  1. 常量

常量部分在方法中會使用,直接截取源碼部分:

  • 默認容量爲10
private static final int DEFAULT_CAPACITY = 10;
複製代碼
  • 定義一個數組,存放元素
private static final Object[] EMPTY_ELEMENTDATA = {};
複製代碼
  • 定義一個數組,用處和上面相似,下文將會說明
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
複製代碼
  • 不序列化的數組,存放元素
transient Object[] elementData;
複製代碼
  • 容器大小
private int size;
複製代碼

  1. 構造器

構造器共有三種:

public ArrayList(int initialCapacity) {} public ArrayList() {} public ArrayList(Collection<? extends E> c) {}

  • 給定容量參數的構造器

用到上面所說的常量中的兩個

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);
        }
    }
複製代碼
  • 無參構造器

這個簡單,直接按照默認容量10,定義一個大小爲10的數組

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
複製代碼
  • 帶泛型參數的構造器
public ArrayList(Collection<? extends E> c) {
        //先將參數轉爲數組類型,toArray()方法重載自AbstractCollection<E>和List<E>類
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            //這是一個官方的bug,說不必定可以返回Object[]類
            if (elementData.getClass() != Object[].class)
                //若是真出現了這個bug,就強制轉回Object[]類
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            //若是傳入的容器參數的容量爲0,就替換爲空數組
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
複製代碼

  1. 經常使用方法

這裏先給出的都是日常使用的方法,不包括迭代器的,關於迭代器的內容是須要單獨拿一篇來講的

先從增刪改查 下手,最後會有一個Demo

增長元素有好幾種,末尾添加,定點添加,以及直接添加一整個容器的元素

末尾添加:

//直接在列表末尾添加元素
    public boolean add(E e) {
		//先擴容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
		//把下標爲size的位置的值設爲e,而後size自增
        elementData[size++] = e;
        return true;
    }
複製代碼

末尾添加其餘容器元素:

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;
    }
複製代碼

定點添加:

這裏有一個檢查數組下標是否越界的方法,須要先說明一下:

private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(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;
		//數組大小加1
        size++;
    }
複製代碼

關於System.arraycopy(),我查了一下源碼中的解釋:

這裏面的參數就是:1. 原始數組 2. 原始位置 3. 目標數組 4. 目標位置 5. 須要移動的元素數量

其實上面添加的方法就是在同一個數組裏面移動元素

而後還有直接添加其餘容器的元素到指定位置

//在指定位置添加其餘容器的元素,可能會拋出空指針異常和數組下標越界異常
    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;
    }
複製代碼

關於刪除,有定點刪除,有刪除特定元素,批量刪除,還有清空列表,思想就是把要刪除的位置的值設爲NULL,而後讓垃圾回收器處理

定點刪除:

首先仍是要給出一個檢查數組是否越界的方法,和上面的相似:

private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(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; // clear to let GC do its work

		//返回刪除的值
        return oldValue;
    }
複製代碼

刪除特定元素

須要先說明一個私有方法,下面會用到:

//快速刪除,不檢查是否有數組下標越界
    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 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;
    }
複製代碼

批量刪除:

這個方法是私有的,實現它的有另外兩個方法 removeAll()retainAll(),批量刪除方法中的布爾類型參數就和這兩個實現方法有關:

傳入一個容器,若是complement爲true,就只保留和容器元素 相同的元素,若是complement爲false,就刪除和容器元素相同的元素

這個測試中,第一個complement爲true,第二個complement爲false:

private boolean batchRemove(Collection<?> c, boolean complement) {
        //複製數組來存放篩選後的數據
        final Object[] elementData = this.elementData;
        //r用來遍歷數組,w用來給數組賦值
        int r = 0, w = 0;
        //判斷結構是否改變
        boolean modified = false;
        try {
            //遍歷
            for (; r < size; r++)
                //若是傳入的容器含有和原先數組相同的元素,就向新數組賦值
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            //若是拋出異常,將r位置以後的元素複製到w位置以後
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            //在篩選完以後,若是w位置後面有空位,就清理掉
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                //結構改變了
                modified = true;
            }
        }
        return modified;
    }
複製代碼

接下來就是調用批量刪除的方法了:

刪除容器元素:

//可能拋出強制類型轉換異常和空指針異常
    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
複製代碼

保留容器元素:

//也可能拋出一樣的異常
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }
複製代碼

清空列表:

removeAll()方法也能夠清空列表,只不過效率低,仍是推薦使用下面這個:

public void clear() {
        modCount++;

        // clear to let GC do its work
        //遍歷數組,設值爲NULL
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }
複製代碼

可能會拋出數組下標越界的錯誤

public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
複製代碼

可能會拋出數組下標越界的異常

public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }
複製代碼
相關文章
相關標籤/搜索