集合源碼分析[3]-ArrayList 源碼分析

歷史文章:
Collection 源碼分析
AbstractList 源碼分析html

介紹

ArrayList是一個數組隊列,至關於動態數組,與Java的數組對比,他的容量能夠動態改變。

繼承關係

  • ArrayList繼承AbstractList
  • 實現了List,RandomAccess,Cloneable,Serializable接口

特色

  • 基於數組實現速度快
  • 實現了RandomAccess接口,提供了隨機訪問功能
  • 實現了Cloneable接口,能被克隆
  • 實現了Serializable接口,支持序列化傳輸
  • 非線程安全(ps:線程安全類:CopyOnWriteArrayList)
  • 適用於頻繁查詢和獲取數據
  • 查詢效率在衆多List中效率仍是很是不錯

構造函數以及經常使用的方法

構造函數

public ArrayList();//無元素默認爲0,有元素初始化默認容量爲10
public ArrayList(Collection<? extends E> c);//默認爲c這個集合的list(淺拷貝)
public ArrayList(int initialCapacity);//設置一個初始化爲initialCapacity集合

注意點java

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            //解決bug問題,因爲其c.toArray()可能出現返回值不爲Object[]的錯誤,因此採用以下方法
            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)這樣一組判斷,查閱資料發現,這是一個bug才這麼判斷的地址,改問題已經在JDK9已經進行了修復了。設計模式

成員變量

private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默認初始化容量10
     */
    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

    /**
     * 容器數據集大小
     *
     * @serial
     */
    private int size;
    /**
     * 該容器可以承受的最大容量
     * 爲何是Integer.MAX_VALUE - 8;
     *  由於有些VM虛擬機會在一個數組中存儲一些頭部信息,因此採用這個值
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

值得注意的點:數組

  1. MAX_ARRAY_SIZE設置爲Integer.MAX_VALUE - 8是由於有些VM虛擬機會在數組中存儲一些頭部信息,從而佔用一些空間,因此-8

經常使用方法

值得注意的方法

trimToSize():

縮小容器大小讓容器釋放多餘的空間,會觸發一次數組的變化緩存

/**
     * 縮小容器大小
     * 例如當你一開始建立了一個100個的List可是你只使用了10個,想將這個容器縮減爲10個
     */
    public void trimToSize() {
        //這個參數和咱們併發控制的時候version一個味道,用於判斷是否併發修改的標誌1
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

clone():

返回數據的淺克隆的實例安全

/**
     * 返回數據的淺克隆的實例
     *
     * @return a clone of this <tt>ArrayList</tt> instance
     */
    public Object clone() {
        try {
            //調用父類的克隆方法->Object的克隆方法
            ArrayList<?> v = (ArrayList<?>) super.clone();
            //拷貝數組,注意是直接經過Arrays的copyOf因此爲淺克隆
            v.elementData = Arrays.copyOf(elementData, size);
            //設置併發version
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            //克隆異常則拋出error
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

toArray():

一樣爲淺拷貝,拷貝出來的數組仍是會被改變的,該方法返回的爲Object[]數組併發

/**
     * 一樣爲淺拷貝,拷貝出來的數組仍是會被改變的
     * 將List轉換成數組
     * Demo[] cloneArr = (Demo[]) demos.toArray(); //ERROR
     *
     * @return an array containing all of the elements in this list in
     *         proper sequence
     */
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

add方法

/**
     * 追加一個元素在列表的最後面
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //這裏會致使其併發版本+1,
        // 由於須要先確認容器大小操做,並肯定是否須要擴容。
        //對數據有修改,於是其併發版本也就會+1
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //設置值
        elementData[size++] = e;
        return true;
    }   
    /**
     * 在index位置後插入元素,並移動後面元素的位置
     * 1. 須要對index後面的全部的元素index+1,須要拷貝工做產生
     * 2. 若是你的List中有大量的這樣的插入工做建議採用
     * @see LinkedList
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        //校驗index
        rangeCheckForAdd(index);
        //肯定擴容權限
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //數組拷貝,耗時工做
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //賦值
        elementData[index] = element;
        //長度
        size++;
    }
  1. 該方法每次都要肯定容器大小,會致使併發版本的count+1
  2. add 方法有兩個
    1. add(E e):直接將數據插入到List的尾部
    2. add(int index,E e):將數據插入到index後面
  3. 若是是插入,則會致使數組進行復制操做,因爲ArrayList基於數組,因此會致使數組複製,而數組複製一定是一個耗時的操做

remove()

public E remove(int index) {
        //校驗
        rangeCheck(index);
        //併發參數+1
        modCount++;
        //獲取舊值
        E oldValue = elementData(index);
        //移動的長度爲=數組長度-須要刪除元素下標-1
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //讓gc回收這個數據的內存
        elementData[--size] = null; // clear to let GC do its work
        //返回舊值
        return oldValue;
    }
  1. 一樣刪除會致使數組複製

subList

該方法主要用於將數組進行分割,對於數組分割後,其實該數組爲淺拷貝操做,若是在該SubList中操做相關數據,將會致使ArrayList中的數據改變!!app

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

ArrayList擴容機制

規則

  1. 若是其數組須要進行擴容,則會擴容爲原數組的1.5倍
  2. 若是用戶指定了容器的大小,且用於指定的數值大於容器的最小容量,則將用於容量做爲該容器容量
  3. 若是容器容量擴容後大於Integer.MAX_VALUE - 8,則會嘗試擴容爲Integer.MAX_VALUE大小

解析

  1. 肯定minCapacity值是否比容器中的數據容量大less

  2. 若是大則擴容,不然什麼也不作dom

  3. 擴容

    1. 若是minCapacity比newCapacity小則直接使用minCapacity做爲擴容容量

    2. 若是其數組個數大於最大的數組的長度,則嘗試使用Integer.MAX_VALUE做爲數組的容器大小

源碼解析

/**
     * 1.肯定minCapacity值是否比容器中的數據容量大
     * 2.若是大則擴容,不然什麼也不作
     * @param minCapacity
     */
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     * 擴容
     * 1. 若是minCapacity比newCapacity小則直接使用minCapacity做爲擴容容量
     * 2. 若是其數組個數大於最大的數組的長度,則嘗試使用Integer.MAX_VALUE做爲數組的容器大小
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //新容器容量擴容爲如今容器的容量的1.5倍
        //eg 舊10個->新15個
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //看看誰大
        if (newCapacity - minCapacity < 0)
            //若是min比new小則直接複製min
            newCapacity = minCapacity;
        //若是新的容器比最大的數組大小還要打
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            //只能複製爲最大容器大小,可是可能會拋出oom
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    /**
     * 大數組容器擴容
     * @param minCapacity
     * @return
     */
    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的序列化操做

因爲Java默認序列化以及反序列化的時候回分別調用對應的writeObject方法以及readObject()方法,因此如下將對這兩個方法進行分析。

6.1 爲何須要自定義序列化規則

因爲在ArrayList中的elementData數組中可能存在一些空的元素(因爲ArrayList擴容機制)

6.2 源碼分析

  1. 序列化操做:
    1. ArrayList內部存儲數據元素爲transient不會被序列化
/**
     * 緩存數據集合
     */
    transient Object[] elementData; // non-private to simplify nested class access
  1. 如何支持序列化操做

    1. 序列化數量
       2. 獲取數據數組,而後使用for循環一個一個序列化該對象到數據中。
    /**
         * Save the state of the <tt>ArrayList</tt> instance to a stream (that
         * is, serialize it).
         * 保證其能夠被序列化到對象中
         *
         * @serialData The length of the array backing the <tt>ArrayList</tt>
         *             instance is emitted (int), followed by all of its elements
         *             (each an <tt>Object</tt>) in the proper order.
         */
        private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
            // Write out element count, and any hidden stuff
            //數據被序列化的數量
            int expectedModCount = modCount;
            //使用默認的模式進行序列化,只序列化非靜態化變量以及非transient修飾的數據
            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++) {
                //寫入每個object數據
                s.writeObject(elementData[i]);
            }
            //若是發現序列化的modCount與expectedModCount多是併發致使
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
        }
  2. 如何進行的反序列化

    1. 從新建立List,而後讀取長度以及多個對象對對象進行相關賦值操做
    /**
         * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
         * deserialize it).
         * 反序列化數據
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            //設置默認的數組長度爲空數組長度
            elementData = EMPTY_ELEMENTDATA;
    
            // Read in size, and any hidden stuff
            //使用默認的模式進行序列化,只序列化非靜態化變量以及非transient修飾的數據
            s.defaultReadObject();
    
            // Read in capacity
            //讀取list長度
            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();
                }
            }
        }

JDK1.8新增的方法

forEach()

lambda循環方法

/**
     * JDK新增方法 ForEach方法
     * @param action
     */
    @Override
    public void forEach(Consumer<? super E> action) {
        //校驗lambda不爲空
        Objects.requireNonNull(action);
        //併發version版本統計
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        //循環每次判斷一下併發參數是否進行了修改,若是進行了修改則直接退出for循環
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            action.accept(elementData[i]);
        }
        //並拋出併發異常
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

spliterator

該方法用於返回進行併發計算時候的分割器

/**
 * Creates a <em><a href="Spliterator.html#binding">late-binding</a></em>
 * and <em>fail-fast</em> {@link Spliterator} over the elements in this
 * list.
 *
 * <p>The {@code Spliterator} reports {@link Spliterator#SIZED},
 * {@link Spliterator#SUBSIZED}, and {@link Spliterator#ORDERED}.
 * Overriding implementations should document the reporting of additional
 * characteristic values.
 * 併發分割方法
 * 懶加載加入,只有當數據
 * @return a {@code Spliterator} over the elements in this list
 * @since 1.8
 */
@Override
public Spliterator<E> spliterator() {
    /**
     *  1. 參數1 this
     *  2. origin
     *  3. fence 當使用的時候才進行初始化
     *  4. 併發參數
     */


    return new ArrayListSpliterator<>(this, 0, -1, 0);
}

removeIf方法

lambda 移除符合某個規則的方法

/**
     * liambad方法移除元素
     * @param filter
     * @return
     */
    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        // figure out which elements are to be removed
        // any exception thrown from the filter predicate at this stage
        // will leave the collection unmodified
        int removeCount = 0;
        //使用這玩意來統計存在的位置
        final BitSet removeSet = new BitSet(size);
        final int expectedModCount = modCount;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            @SuppressWarnings("unchecked")
            final E element = (E) elementData[i];
            if (filter.test(element)) {
                removeSet.set(i);
                removeCount++;
            }
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }

        // shift surviving elements left over the spaces left by removed elements
        final boolean anyToRemove = removeCount > 0;
        if (anyToRemove) {
            //新數組的大小爲以前的數組長度-須要移除元素的個數
            final int newSize = size - removeCount;
            //執行清除工做
            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                i = removeSet.nextClearBit(i);
                elementData[j] = elementData[i];
            }
            //釋放gc
            for (int k=newSize; k < size; k++) {
                elementData[k] = null;  // Let gc do its work
            }
            this.size = newSize;
            //併發version檢查
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            //併發值+1
            modCount++;
        }

        return anyToRemove;
    }

ArrayListSpliterator

其爲併發分割器,用於咱們使用併發調用parallelStream方法時候調用該方法

static final class ArrayListSpliterator<E> implements Spliterator<E> {

    private final ArrayList<E> list;
    private int index; // current index, modified on advance/split
    private int fence; // -1 until used; then one past last index
    private int expectedModCount; // initialized when fence set

    /** Create new spliterator covering the given  range */
    ArrayListSpliterator(ArrayList<E> list, int origin, int fence,
                         int expectedModCount) {
        this.list = list; // OK if null unless traversed
        this.index = origin;
        this.fence = fence;
        this.expectedModCount = expectedModCount;
    }

    /**
     * 當獲取的時候才進行fence的初始化操做
     * @return
     */
    private int getFence() { // initialize fence to size on first use
        int hi; // (a specialized variant appears in method forEach)
        ArrayList<E> lst;
        if ((hi = fence) < 0) {
            //以前的數組爲空則說明沒有進行分割,則從0開始
            if ((lst = list) == null)
                hi = fence = 0;
            else {
                //不然則
                expectedModCount = lst.modCount;
                //則將該值賦值爲lst的長度
                hi = fence = lst.size;
            }
        }
        return hi;
    }

    /**
     * 嘗試分割
     * 一、總長度爲數組長度
     * 二、分割成兩份
     * 三、二分法分割
     * 三、中間值爲數組長度+分割後的數組長度,就是二分法啦
     * @return
     */
    public ArrayListSpliterator<E> trySplit() {
        int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
        return (lo >= mid) ? null : // divide range in half unless too small
            new ArrayListSpliterator<E>(list, lo, index = mid,
                                        expectedModCount);
    }

    /**
     * 併發執行操做
     * @param action
     * @return
     */
    public boolean tryAdvance(Consumer<? super E> action) {
        if (action == null)
            throw new NullPointerException();
        int hi = getFence(), i = index;
        //若是沒有超過這個分割的長度則繼續操做不然返回false
        if (i < hi) {
            index = i + 1;
            @SuppressWarnings("unchecked") E e = (E)list.elementData[i];
            action.accept(e);
            //若是出現了併發改變,則拋出異常
            if (list.modCount != expectedModCount)
                throw new ConcurrentModificationException();
            return true;
        }
        return false;
    }

    /**
     * 併發ForEach輸出
     * @param action
     */
    public void forEachRemaining(Consumer<? super E> action) {
        int i, hi, mc; // hoist accesses and checks from loop
        ArrayList<E> lst; Object[] a;
        if (action == null)
            throw new NullPointerException();
        if ((lst = list) != null && (a = lst.elementData) != null) {
            if ((hi = fence) < 0) {
                mc = lst.modCount;
                hi = lst.size;
            }
            else
                mc = expectedModCount;
            if ((i = index) >= 0 && (index = hi) <= a.length) {
                for (; i < hi; ++i) {
                    @SuppressWarnings("unchecked") E e = (E) a[i];
                    action.accept(e);
                }
                if (lst.modCount == mc)
                    return;
            }
        }
        throw new ConcurrentModificationException();
    }

    public long estimateSize() {
        return (long) (getFence() - index);
    }

    public int characteristics() {
        return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
    }
}

ArrayList遍歷

8.1 使用迭代器進行遍歷

Iterator<Double> iterator = list.iterator();
while (iterator.hasNext()){
   Double next = iterator.next();
   System.out.println(next);
}

使用fori進行遍歷

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

8.3 使用for進行遍歷

for (Double next : list) {
  System.out.println(next);
}

8.4 使用流進行訪問(JDK1.8)

list.forEach(System.out::println);

8.5 以上幾種模式的效率比較

測試代碼以下

package cn.lonecloud;

import java.time.Duration;
import java.time.Instant;
import java.util.*;

/**
 * @author lonecloud
 * @version v1.0
 * @date 2019/4/3 19:56
 */
public class ListTest {
    public static void main(String[] args) {
        int len=20000000;
        List<Integer> list=new ArrayList<>(len);
        for (int i = 0; i < len; i++) {
            list.add(i);
        }
        test(list);
    }
    public static void test(List<Integer> list){

        long itrBegin = System.currentTimeMillis();
        //1. 使用迭代器
        Iterator<Integer> iterator = list.iterator();
        Integer a=0;
        while (iterator.hasNext()){
            Integer next = iterator.next();
            a=next;
        }
        long itrEnd=System.currentTimeMillis();
        long foriStart=System.currentTimeMillis();
        //2. fori
        for (int i = 0; i < list.size(); i++) {
            a=list.get(i);
        }
        long foriEnd=System.currentTimeMillis();

        long forStart=System.currentTimeMillis();
        //3. for
        for (Integer next : list) {
            a=next;
        }
        long forEnd=System.currentTimeMillis();

        long streamstart=System.currentTimeMillis();

        //4. stream
        list.forEach((value)->{
            Integer b=value;
        });

        long streamEnd=System.currentTimeMillis();
        System.out.println("迭代器時間:"+(itrEnd-itrBegin));
        System.out.println("fori時間:"+(foriEnd-foriStart));
        System.out.println("for時間:"+(forEnd-forStart));
        System.out.println("stream時間:"+(streamEnd-streamstart));
    }
}

結果:

迭代器時間:31
fori時間:32
for時間:32
stream時間:74

總結:

效率:

  1. fori和for和迭代器大體相同,因爲事先了RandomAccess

  2. stream時間高一些,緣由是須要進行更多的方法調用產生的時間

使用到的設計模式

  • 迭代器設計模式
  • 模板設計模式

總結

  1. ArrayList是基於數組的集合,適合循環迭代多的場景,不適合修改多的場景

  2. 在使用ArrayList時候須要注意在建立的時候(預估一下你須要的容器大小)

    因爲若是你在使用的時候超過了初始化容量(10),這將會致使容器進行一次(容器擴容),而數組複製是一件很是耗時的操做

  3. ArrayList中的clone()方法以及copy方法,都是淺克隆的。

  4. 在通常狀況下,若是集合容器出現容量不足須要擴容的時候,其集合會擴容爲原集合的1.5倍大小

  5. 若是須要將List轉換成數組,推薦使用泛型方法T[] toArray(T[] a)而不是Object[] toArray()方法

  6. 若是涉及在指定位置上插入指定元素的操做,若是這種操做比較多,推薦使用LinkedList而不是使用ArrayList,由於你每次在指定的位置上插入元素會致使數組拷貝操做。

  7. 若是List涉及到頻繁修改的時候,建議使用LinkedList,而不是使用ArrayList。

  8. ArrayList是一個非線程安全類,若是須要設計到線程安全,請使用併發包相關的類

  9. subList(int fromIndex, int toIndex)方法返回的SubList類,其中若是你對該List操做時候,原集合也會改變

相關文章
相關標籤/搜索