Java源碼之旅(1) - ArrayList

技術在學習中成長,源碼的世界沒有你想象的那麼複雜

前言

2018年的五月,開始java的源碼學習之旅,從簡單的角度去理解java的源碼,前幾天在學習交流中正好看了一下java集合的源碼,才發現源碼並沒有想象中的那麼難以理解,所以,源碼之旅從java的集合類開始咯。

本章的源碼版本爲:JDK1.8

類的關係

要理解ArrayList的源碼,我們就需要從它的關係開始,ArrayList繼承了AbstractList,實現了List接口,我們從UML圖可以看出:

虛線箭頭表示實現接口,實線箭頭表示繼承關係

ArrayList簡介

ArrayListjava中最常用的集合類了,說到ArrayList,我們不得不說說LinkedList,因爲他們都是從Collection派生而來的,都是用來存放對象的序列的集合類,ArrayList相比與LinkedList有什麼優劣呢?

  • ArrayList:

    • 優點:隨機訪問元素的速度快

    • 缺點:從中間插入和移除元素比較慢

  • LinkedList

    • 優點:從中間插入和移除元素速度快

    • 缺點:隨機訪問元素的速度比較慢

接下來我們就從源碼的角度去理解一下爲什麼有這些優缺點。

源碼分析

ArrayList的初始化

ArrayList有三個構造方法

//默認初始容量
private static final int DEFAULT_CAPACITY = 10;
//空的元素數組
private static final Object[] EMPTY_ELEMENTDATA = {};
//初始容量空的元素數組
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存放元素的數組
transient Object[] elementData; // non-private to simplify nested class access
//數組的大小
private int size;

// 空參的構造方法
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // -- (1)
    }

//指定初始容量的構造方法
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity]; // -- (2)
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA; // -- (3)
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

//初始化給定一個集合的構造函數
public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();    // -- (4)
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);  // -- (5)
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

從源碼中我們可以看出,ArrayList其實底層使用的是數組transient Object[] elementData 這個變量就是用來存放對象的一個數組。

  • ArrayList的空參構造函數:也就是默認的構造函數,當new ArrayList()的時候調用這個方法,可以看出將elementData變量地址指向了DEFAULTCAPACITY_EMPTY_ELEMENTDATA這個初始容量爲10,並且爲空的元素數組;如步驟(1)

  • ArrayList的指定大小的構造函數:當初始化一個指定大小的new ArrayList(int)的時候調用該方法,這個方法首先對initialCapacity參數進行判斷,如果大於0,那麼創建一個指定大小的數組(2)如果等於0,創建一個空的數組(3);否則就判處異常;

  • 初始化給定一個集合的構造函數:如果初始化的時候,給定一個集合對象,那麼將這個集合轉換爲數組 (4),然後對這個數組的長度進行判斷,如果數組不等於0,那麼調用Arrays.copyOf(elementData, size, Object[].class)方法(5),這個方法是一個核心方法,這個方法就是初始化一個大小爲等於當前數組的一個新的數組,然後將對象copy到新的數組中,然後將內存地址指定給elementData,從下面的Arrays.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;
    }

copyOf方法首先判斷兩個對象的類型,如果類型一致,那麼直接創建一個同大小的數組;如果類型不一致,則調用Array.newInstance指定類型進行初始化這個數組,當然,大小也是一致的; 最後調用System.arraycopy將參數數組copy到新的目標並返回。

ArrayList的常用方法之 add

我們先看一下源碼:

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!! -- (1)
        elementData[size++] = e;
        return true;
    }

public void add(int index, E element) {
        rangeCheckForAdd(index);//-- (2)

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index); //-- (3)
        elementData[index] = element;
        size++;
    }

ArrayList有兩個add方法,第一個方法就是按順序將對象插入到尾部,第二個方法就是從中間插入對象。

add(E e) 方法首先會判斷數組的容量是否超過極限ensureCapacityInternal(size + 1),這個方法首先會進行容量的判斷,如果超過了極限,創建一個新的數組,大小是舊數組1.5倍,然後將舊數組中的對象全部拷貝到新的數組(1),等下會詳細解析這個方法。最後將參數對象插入到數組中,返回true。

add(int index, E element)首先會調用rangeCheckForAdd(index)進行index的是否越界的驗證(2),然後調用上一個方法中一樣的判斷容量是否超過極限的方法,下一步就是一個核心的方法System.arraycopy,這個方法我們在ArrayList初始化中已經講過了,但是這裏不太一樣:

  • elementData : 源數組

  • index:源數組起始位置

  • elementData:目標數組

  • index + 1:目標數組起始位置

  • size - index:複製數組元素數目

從源碼中可以看出,當我們往一個ArrayList中間插入一個對象的時候,index索引處後面的索引往後移動一位,最後把索引爲index空出來,並將element賦值給它。這樣一來我們並不知道要插入哪個位置,所以會進行匹配那麼它的時間賦值度就爲n。

接下來看一下ensureCapacityInternal(size + 1)這個方法的調用鏈:

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);//oldCapacity >> 1 就是除以2
  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);
}

ensureCapacityInternal(int minCapacity)方法中判斷當前數組中的元素是否爲空,如果爲空則給定一個最大的值,然後調用ensureExplicitCapacity(minCapacity),這個方法主要是判數組容量是否超過極限,如果超過極限調用grow(int minCapacity),這個方法就是擴容方法,該方法會創建一個比原數組大1.5倍的新數組,然後將原數組中的所有對象copy到新的數組中。

ArrayList的常用方法之 remove

remove方法其實跟從中間插入對象的add方法有很大的相似之處,如果我們刪除某一個元素,將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;
    }

ArrayList的常用方法之 get

ArrayListget方法非常直觀的就能理解了,廢話不多說,直接看代碼:

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

   return elementData(index);
 }

檢查index合法性,然後從數組中取出對象並返回,是不是很簡單呢?

總結

本章列舉了一些ArrayList常用的方法,瞭解到ArrayList底層其實是一個對象數組,以及從中間插入對象和移除對象比較慢的原因,從這些方法出發,理解ArrayList其他的方法會很簡單了。下一章講一講LinkedList的源碼。

發佈了84 篇原創文章 · 獲贊 551 · 訪問量 129萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章