ArrayList源碼閱讀

概述

ArrayList是JAVA集合類中一個最爲基礎最爲使用普遍的集合,本文將基於JDK1.8來解讀ArrayList的源碼實現java

ArrayList的底層數據結構與基本原理

ArrayList的基礎結構是數組,因此ArrayList和數組具備一樣的性質:算法

  1. 數據插入數據刪除效率較低
  2. 數據查詢效率較高

那麼問題來了,數組是固定大小的,ArrayList大小是可變的,內部是如何維護,如何實現的呢。其是從原理上來講很簡單,那就是若是內部數組長度不夠了,那就建立一個新的更大的數組,把原來的數據拷過去便可,具體代碼如何實現,那就讓咱們來詳細的看一下源碼吧。api

ArrayList源碼

繼承關係

ArrayList繼承關係以下圖:
ArrayList類繼承關係.png數組

  1. 繼承:ArrayList繼承了AbstractList,這是list的一個基礎抽象類,AbstractList則繼承了AbstractCollection。AbstractCollection是全部集合類的父類。從圖中咱們能夠看到全部的集合類都繼承了Iterable類,這個類是迭代器類,爲全部的集合類都提供了ForEach方法。
  2. 實現:ArrayList實現瞭如下幾個類:數據結構

    1. List: 這裏很奇怪,由於AbstractList也實現了List,在這裏重複實現究竟有什麼用意呢?
    2. Serializable:這個類是爲了序列化
    3. RandomAccess:這個類是用來標記使用,具備這個標記的類具有如下特色:能夠快速訪問,而且使用下標訪問會更快。例如ArrayList就實現了這個類,而LinkedList則沒有。這個標記在某些狀況下會有做用:例如在實現二分查找的時候,若是所傳入的集合類實現了這個類則直接使用下標訪問元素,使得算法實現效率更高,不然用迭代器來訪問元素,會比下標訪問更加快。好比Collections裏的二分查找方法,就進行了這樣的判斷:dom

      public static <T>
      int binarySearch(List<? extends Comparable<? super T>> list, T key) {
          if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
              return Collections.indexedBinarySearch(list, key);
          else
              return Collections.iteratorBinarySearch(list, key);
      }
    4. Cloneable:實現該接口以使用clone方法

類屬性

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

    // 默認的容量
    private static final int DEFAULT_CAPACITY = 10;

    // 空數組用於給初始化大小爲0的狀況進行直接賦值,或者是進行清空list操做時候直接進行賦值
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 默認容量空數組,用於無參構造函數的初始化中
    // 在有了EMPTY_ELEMENTDATA以後還要DEFAULTCAPACITY_EMPTY_ELEMENTDATA的緣由是想要區分
    // 這兩種狀況:1.用無參構造函數初始化的空數組 
    // 2.用有參構造函數初始化,而且設定數組初始化大小爲0的空數組
    // 若是不作區分這兩種狀況就會混在一塊兒無法區分,而後會致使在擴容時產生出乎調用者意料的狀況
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // ArrayList存儲數據的數組,加上了transient進行修飾說明這個數組將不會被序列化
    // ArrayList的元素序列化反序列化是靠readObject和writeObject來實現的
    transient Object[] elementData;

    // ArrayList的大小
    private int size;
}

類構造函數

  1. 無參構造函數ide

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
  2. 數組容量初始化函數

    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. 用集合初始化學習

    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        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);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

    其他的兩個構造函數比較簡單就不贅述了,可是這個構造函數有一點特殊的地方。有人看到源碼後,想問這句代碼if (elementData.getClass() != Object[].class)究竟有什麼用?咱們看到註釋上寫了see 6260652,那就去java的官網一探究竟。看到官網給出的bug記錄以下:ui

    The Collection documentation claims that

    collection.toArray()

    is "identical in function" to

    collection.toArray(new Object[0]);

    However, the implementation of Arrays.asList does not follow this: If created with an array of a subtype (e.g. String[]), its toArray() will return an array of the same type (because it use clone()) instead of an Object[].

    If one later tries to store non-Strings (or whatever) in that array, an ArrayStoreException is thrown.

    簡單翻譯下就是collection.toArray()這個方法按照規範應該等價於collection.toArray(new Object[0]),可是各個collection有本身的實現可能會有一些意外狀況出現,好比Arrays.asList,下面有一個例子:

    public static void main(String[] args) {
            String[] a = {"a", "b", "c"};
    
            List l = Arrays.asList(a);
    
            System.out.println(l.toArray());
            System.out.println(l.toArray(new Object[0]));
        }

    l.toArray()返回類型是String[]l.toArray(new Object[0])返回值則爲Object[]。若是不加這句代碼,內部的elementData就會被賦值爲String數組,這是咱們所不想看到的,因此須要多一層判斷,保證這個數組的類型永遠是Object[]。固然,這算是一個bug,而且在後續版本中(JAVA9)中被修復了。

核心方法

  1. 說到ArrayList的核心方法,那必須是他的擴容方法grow

    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. oldCapacity是elementData的長度(注意elementData的長度是指內部數組的長度,size是list元素的數量,因此這兩個數值每每是不一致的
    2. newCapacity是預計想要達到的數組容量大小,通常狀況下是擴容1.5倍,即oldCapacity + (oldCapacity >> 1),使用右移運算比乘法效率更高。
    3. 接下來將預計擴容的大小與最小擴容值進行比較,取較大的值。這裏有一個問題爲何比較大小不寫成if (newCapacity < minCapacity)而要寫成if (newCapacity - minCapacity < 0),這是由於若是舊有的容量已經夠大的話,進行1.5倍擴容後newCapacity可能已經由於數字過大溢出變成了負數,若是用newCapacity < minCapacity來比較的話那就會永遠是true,這不符合咱們的預期,咱們的預想應該是既然新的容量已經溢出了,那麼咱們應該是取到溢出前的極大值,這麼寫,就會致使取到minCapacity。而newCapacity - minCapacity就不同了,即便newCapacity溢出也不會影響判斷。
    4. 判斷newCapacity是否會超出設定的最大值,若是超出就調用如下方法來判斷到底擴容到多大:

      private static int hugeCapacity(int minCapacity) {
          if (minCapacity < 0) // overflow
              throw new OutOfMemoryError();
          return (minCapacity > MAX_ARRAY_SIZE) ?
              Integer.MAX_VALUE :
              MAX_ARRAY_SIZE;
      }
    5. 使用Arrays.copyOf方法來進行數組的擴容
  2. 在某些狀況下每一次擴容1.5倍顯然沒法知足咱們的需求,當咱們預計一次性將插入大量數據的時候,頻繁的擴容會致使頻繁的建立新的數組,而且把舊數據遷移到新的數組之中。這個效率是很是低下的。因此咱們有兩種方法解決這個問題:一種就是在建立ArrayList的時候就直接用數組容量來建立數組,可是這種方法因爲咱們建立ArrayList的時候未必開始就會插入這麼多數據,因此可能會致使內存的浪費,另外一種方法就是使用ensureCapacity方法:

    public void ensureCapacity(int minCapacity) {
    
            // 最小容量擴展數,通常爲DEFAULT_CAPACITY,也就是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
                // supposed to be at default size.
                : DEFAULT_CAPACITY;
    
            // 若是須要的擴容大小大於minExpand,則擴容至minCapacity,不然就不進行擴容
            if (minCapacity > minExpand) {
                ensureExplicitCapacity(minCapacity);
            }
        }
    
        private static int calculateCapacity(Object[] elementData, int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            return minCapacity;
            }
    
        private void ensureCapacityInternal(int minCapacity) {
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
        }
    
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
  3. ArrayList的添加方法以下,共有5種:

    public E set(int index, E element) {
            rangeCheck(index);
    
            E oldValue = elementData(index);
            elementData[index] = element;
            return oldValue;
        }
    
        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    
        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++;
        }
    
        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;
        }

    實現方法都比較簡單,有幾個地方須要注意一下:

    1. 在固定的index插入元素的方法都會在方法最開始調用一個名爲rangeCheck的方法來檢查是否越界,方法以下:

      private void rangeCheck(int index) {
              if (index >= size)
                  throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
          }
      
          private void rangeCheckForAdd(int index) {
              if (index > size || index < 0)
                  throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
          }

      有人可能會好奇爲何要作這個校驗,主要的緣由其實在前面說過了,存儲元素的數組大小不等於size的大小,舉個例子,當你的ArrayList只有一個元素的時候,你的size爲1,可是你的內部數組大小多是default的10,這個校驗能夠避免你把數據插入到了奇怪的地方。

    2. 在插入大量數據的時候它會調用內部使用的ensureCapacityInternal來減小擴容
  4. get方法:

    public E get(int index) {
            // 校驗是否越界(校驗理由同上)
            rangeCheck(index);
    
            // 獲取元素
            return elementData(index);
        }
    
        E elementData(int index) {
            return (E) elementData[index];
        }
  5. 刪除方法:

    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;
        }
    
        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 removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
    
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }
    
    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)
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            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;
    }

    刪除方法沒什麼特別的地方,無非是找到後刪除,而後數組元素移動等,惟一的一個特別的地方是,刪除的時候循環中是使用下標循環查找刪除,而不是使用迭代器循環,緣由就是,數組特性:使用下標訪問更快,這也是爲何ArrayList實現了RandomAccess

  6. 序列化方法:

    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
            // Write out element count, and any hidden stuff
            int expectedModCount = modCount;
            s.defaultWriteObject();
    
            // Write out size as capacity for behavioural compatibility with clone()
            s.writeInt(size);
    
            // Write out all elements in the proper order.
            for (int i=0; i<size; i++) {
                s.writeObject(elementData[i]);
            }
    
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
        }
    
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            elementData = EMPTY_ELEMENTDATA;
    
            // Read in size, and any hidden stuff
            s.defaultReadObject();
    
            // Read in capacity
            s.readInt(); // ignored
    
            if (size > 0) {
                // be like clone(), allocate array based upon size not capacity
                int capacity = calculateCapacity(elementData, size);
                SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
                ensureCapacityInternal(size);
    
                Object[] a = elementData;
                // Read in all elements in the proper order.
                for (int i=0; i<size; i++) {
                    a[i] = s.readObject();
                }
            }
        }

    集合元素存儲在數組中在序列化的時候須要特殊處理,否則會有問題,所以把elementData設置爲transient防止數組被序列化,而且本身實現readObjectwriteObject來實現一個能夠完美處理數組元素的序列化方法

總結

ArrayList是代碼中出現率極高的數據結構,在平常使用中能夠說是大量使用,而且本人也自認爲對其瞭如指掌,可是直到慢慢沿襲它的源碼,才發現他的不少設計理念以及一些不經常使用api於我來講實際上是很是陌生的,所以,我一邊查資料一邊學習它的源碼,試圖可以學到一些東西。我在此把我閱讀源碼的一些體會和心得記錄下來,但願可以與你們一塊兒學習進步。

相關文章
相關標籤/搜索