Java集合(一) —— ArrayList

午夜12點發表於2018-03-26

ArrayList

ArrayList介紹

我們先看一下ArrayList的繼承關係圖:

Java集合(一) —— ArrayList
①.從圖中我們可以看出來ArrayList實現了Serializable介面表示支援序列化,實現了RandomAccess介面表示支援隨機訪問,實現了Cloneable介面,表示能被克隆.
②.ArrayList允許所有元素,包括null元素可重複.往ArrayList不斷新增元素,其容量也自動增長,可以使用ensureCapacity()方法在新增大量元素前來增加ArrayList例項的容量,這樣可以減少增量重新分配的數量.
③.ArrayList執行緒不安全,可以用 Collections.synchronizedList()使ArrayList執行緒安全,或者使用結構類似的CopyOnWriteArrayList(若讀的場景多用CopyOnWriteArrayList,寫場景多用Collections.synchronizedList,vector不建議用).
④.當某一個執行緒通過iterator遍歷ArrayList的過程中,若ArrayList中元素被其他執行緒所改變了;那麼執行緒A訪問集合時,就會丟擲ConcurrentModificationException異常,產生fail-fast事件.
⑤.ArrayList刪除和插入指定索引方法的最壞估計時間為O(n),更確切地說是O(n-index),即在索引為0操作時所需時間最長,在末尾位置操作所需時間最少.
不介紹函數語言程式設計

原始碼解析

ArrayList欄位



    /**
     * 序列號
     */
    private static final long serialVersionUID = 8683452581122892189L;
    /**
     * 預設容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空陣列,定義容量為0的有參構造,返回此空陣列public ArrayList(0)
     * 或者定義長度為0集合的有參構造,也會返回
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 預設的空陣列,無參構造返回此空陣列
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 儲存新增到ArrayList中的元素,不可序列化,可以看出ArrayList基於陣列實現
     */
    transient Object[] elementData;

    /**
     * ArrayList元素數量(非容量)
     */
    private int size;  
    
    /**
     * 最大容量,若還是不夠用可以擴容最大Integer.MAX_VALUE
     * 因為索引是int型,所以最大容量為Integer.MAX_VALUE
     * 為什麼減8,不是減其他數字?
     *[有點偏而且我看得也很懵。。](https://stackoverflow.com/questions/35756277/why-the-maximum-array-size-of-arraylist-is-integer-max-value-8),
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
複製程式碼

構造方法


     /**
     * 若知道陣列大小,可以直接用此構造
     *  可能丟擲java.lang.OutOfMemoryError: Requested array size exceeds VM limit
     * @param 初始容量
     * @throws IllegalArgumentException 若容量為負則拋異常
     */
    public ArrayList(int initialCapacity) {
        // 容量大於0時 
        if (initialCapacity > 0) {
            // 建立一個此大小的Object陣列賦給elementData
            this.elementData = new Object[initialCapacity];
        
        // 容量為0時
        } else if (initialCapacity == 0) {
            // 將空陣列賦給elementData
            this.elementData = EMPTY_ELEMENTDATA;
        // 容量小於0時丟擲IllegalArgumentException異常    
        } else {
            throw new IllegalArgumentException("Illegal Capacity: " +
                    initialCapacity);
        }
    } 
    
    /**
     * 無參構造,在新增操作時當elementData為空陣列時會初始化其容量設定成10
     */
    public ArrayList() {
        // 將空陣列賦給elementData
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    /**
     * 引數Collection的構造方法
     *
     * @param c 元素被放入此ArrayList中的集合c
     * @throws NullPointerException 若集合c為空則拋異常
     */
    public ArrayList(Collection c) {
        //集合轉Object[]陣列
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toarray可能不返回Object[](見JAVA BUG編號6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 空陣列賦給elementData
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
複製程式碼

JAVA BUG編號6260652


     /** List的toArray()方法   預期返回Object[],可能向上轉型返回子型別,如下面的返回
      *  String[]陣列,這也就意味著我們只能往此陣列裡放入String型別元素
      */
     public static void main(String[] args) {
        List l = Arrays.asList(args);
        System.out.println(l.toArray().getClass()); //返回String[]
    }
複製程式碼

CRUD

Create

ArrayList新增(public)有如下幾個方法:
public boolean add(E e) //末尾插入某元素
public void add(int index, E element) //指定位置插入某元素
public boolean addAll(Collection<? extends E> c)//末尾插入集合(順序不變)
public boolean addAll(int index, Collection<? extends E> c)//指定位置插入集合

ArrayList是一個動態陣列,其容量會自動增長,那它如何實現的?我們來看一下add()方法,在其給當前末尾元素後一個位置新增元素時,先執行了ensureCapacityInternal方法:


     public boolean add(E e) {
        //確定ArrayList容量大小
        ensureCapacityInternal(size + 1);
        //ArrayList長度加一,在size+1位置上新增元素
        elementData[size++] = e;
        return true;
    } 
複製程式碼

在ensureCapacityInternal方法裡會先調calculateCapacity計算所需容量,若elmentData為空陣列時容量會設定成10,若不是返回操作所需容量.


     private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
     }
     
     private static int calculateCapacity(Object[] elementData, intminCapacity){
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
複製程式碼

最後呼叫ensureExplicitCapacity方法確認是否擴容.方法裡先是將modCount加1(modCount記錄ArrayList修改次數,用來實現fail-fast機制的).再判斷所需容量是否超過當前容量,若大於則擴容


    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // 
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
複製程式碼

擴容方法:


     private void grow(int minCapacity) {
        //獲取ArrayList中elementData陣列的記憶體空間長度
        int oldCapacity = elementData.length;
        //擴容至原容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //若新容量小於所需容量,就將容量直接設定成所需容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //若新容量大於定義的最大值檢查是否溢位    
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //Arrays.copy進行陣列拷貝(方法裡面會會呼叫System.arraycopy屬於淺拷貝)   
        elementData = Arrays.copyOf(elementData, newCapacity);
    } 
    
    /**
     * 檢查是否溢位,若沒有溢位,返回int最大值或定義的最大值
     */
    private static int hugeCapacity(int minCapacity) {
        //記憶體溢位
        if (minCapacity < 0) 
            throw new OutOfMemoryError();
        //大於定義的最大值返回int最大值,小於返回定義的最大值    
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
複製程式碼

其餘新增方法類似,稍微略帶下:


    /**
     * 指定位置新增元素
     */
    public void add(int index, E element) {
        //判斷index是否越界
        rangeCheckForAdd(index);
        //判斷是否需要擴容
        ensureCapacityInternal(size + 1);
        //public static native void arraycopy(Object src,  int  srcPos,
        // Object dest, int destPos, int length); 本地方法
        // src:源陣列,srcPos:源陣列要複製的起始位置,dest:目標陣列
        // destPos:目標陣列放置的起始位置,length:複製長度
        // index位置及其後面的元素往後移一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //指定位置賦值
        elementData[index] = element;
        //ArrayList長度加一
        size++;
    } 
    
    /**
     * 末尾依次新增集合所有元素,add(Collection)與addAll(Collection)區別在於add * 是新增集合,addAll是新增集合所有元素
     */
    public boolean addAll(Collection c) {
        //引數集合轉為陣列
        Object[] a = c.toArray();
        //獲取陣列長度
        int numNew = a.length;
        //判斷是否擴容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        //將a的第0位開始拷貝至elementData的size位開始,拷貝長度為numNew
        System.arraycopy(a, 0, elementData, size, numNew);
        // size增加numNew
        size += numNew;
        //空集合返回false,不為空返回true
        return numNew != 0;
    }
    
    /**
     * 指定位置插入集合所有元素
     */
    public boolean addAll(int index, Collection c) {
        //判斷index是否越界
        rangeCheckForAdd(index);
        //引數集合轉為陣列
        Object[] a = c.toArray();
        //獲取陣列長度
        int numNew = a.length;
        //判斷是否擴容
        ensureCapacityInternal(size + numNew);  
        int numMoved = size - index;
        //若是末尾插入直接插入集合,若不是先移動原來集合中元素再插入以免被覆蓋
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
        System.arraycopy(a, 0, elementData, index, numNew);
        // size增加numNew
        size += numNew;
        //空集合返回false,不為空返回true
        return numNew != 0;
    }
複製程式碼

Retrieve

ArrayList查詢(public)有如下幾個方法:
public boolean contains(Object o)//查詢是否包含某元素
public int indexOf(Object o)//查詢某元素首次出現在ArrayList的索引


    /**
     * 內部呼叫了indexOf,若-1則表示沒有,不小於0表示有
     */
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    } 
    
    /**
     * for迴圈從索引為0往後開始遍歷ArrayList,返回元素首次出現位置,若不存在返回 -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 E get(int index)//查詢此索引位置的元素


    public E get(int index) {
        // 檢查是否越界
        rangeCheck(index);
        // 返回ArrayList的elementData陣列index位置的元素
        return elementData(index);
    }
    
    返回指定位置的值
    E elementData(int index) {
        return (E) elementData[index];
    } 
複製程式碼

public boolean isEmpty() //查詢ArrayList大小是否為0


    /**
     * ArrayList大小為0返回true,否則返回false
     */
    public boolean isEmpty() {
        return size == 0;
    } 
複製程式碼

public int lastIndexOf(Object o) //查詢某元素最後一次出現在ArrayList的索引


    /**
     * for迴圈從索引為size-1往前開始遍歷ArrayList,返回元素首次出現位置,若不存在返回 -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 int size() //返回ArrayList的長度


    /**
     * 直接返回size
     */
    public int size() {
        return size;
    } 
複製程式碼

public List subList(int fromIndex, int toIndex)


    /**
     * 返回指定[fromIndex, toIndex)區間的集合檢視,fromIndex == toIndex時返回空集合
     * 此子集合具有集合所有操作
     */
    public List subList(int fromIndex, int toIndex) {
        //越界檢查,非法引數檢查
        subListRangeCheck(fromIndex, toIndex, size);
        //返回subList有參構造
        return new SubList(this, 0, fromIndex, toIndex);
    } 
    
    /**
     * 內部類,實現了RandomAccess提供快速訪問
     */
    private class SubList extends AbstractList implements RandomAccess {
        private final AbstractList parent;
        private final int parentOffset;
        private final int offset;     
        int size;
        /**  
         * 從這裡可以看出,subList用的是父集合引用,即對subList操作的同時會影父集合
         */
        SubList(AbstractList parent,
                int offset, int fromIndex, int toIndex) {
            this.parent = parent;
            this.parentOffset = fromIndex;
            this.offset = offset + fromIndex;
            this.size = toIndex - fromIndex;
            this.modCount = ArrayList.this.modCount;
        }
    }
複製程式碼

Update

ArrayList更新(public)有如下幾個方法:
public void ensureCapacity(int minCapacity)


    /**
     * 此方法用於若新增大量元素,可以先呼叫此方法分配多點記憶體,從而避免多次重新分配記憶體
     */
    public void ensureCapacity(int minCapacity) {
        // 最小擴充容量,預設是 10  
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            ? 0
            : DEFAULT_CAPACITY;
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    } 
複製程式碼

public E set(int index, E element)


    /**
     * 設定index位置元素的值,返回被替換元素
     */
    public E set(int index, E element) {
        //校驗是否越界
        rangeCheck(index);
        //取出舊值
        E oldValue = elementData(index);
        //替換元素
        elementData[index] = element;
        //返回被替換元素
        return oldValue;
    }
複製程式碼

public void trimToSize()


    /**
     * 將容量修改成ArrayList實際大小,用來減少記憶體空間資源浪費
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    } 
複製程式碼

Delete

ArrayList刪除(public)有如下幾個方法:
public E remove(int 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; 
        return oldValue;
    } 
複製程式碼

public boolean remove(Object o) //刪除某元素,只會刪除第一次出現的


    /**
     * for迴圈遍歷ArrayList,若集合存在此元素呼叫fastRemove快速移除
     */
    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;
    } 
    
    /**
     * 與remove(int index)區別,其沒有校驗是否越界,也不用返回被替換元素
     */
    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; 
    }
複製程式碼

public boolean removeAll(Collection<?> c) //刪除ArrayList與c中的相同元素


    public boolean removeAll(Collection c) {
        //當c為null拋空指標異常
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    } 
    
    /**
     * 批量移除
     * complement為true時保留ArrayList與集合c的相同元素
     * false時移除相同元素  
     */
    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++)
                //false記錄不同值,true記錄相同值
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // 防止丟擲異常沒有遍歷完,保留未遍歷的元素
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            // 若不是全被替換,將w及其位置後面的元素置為null
            if (w != size) {
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                //記錄改變次數
                modCount += size - w;
                //設定大小
                size = w;
                //修改成功
                modified = true;
            }
        }
        return modified;
    }
複製程式碼

public boolean retainAll(Collection<?> c) //保留ArrayList和集合c中的相同元素`


    public boolean retainAll(Collection c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    } 
複製程式碼

public void clear()


    /**
     * 清空所有元素
     */
    public void clear() {  
        modCount++;  
        for (int i = 0; i < size; i++)  
            elementData[i] = null;  
        size = 0;  
    }   
複製程式碼

轉換陣列


    /**
     * 用Arrays.copyOf()轉為陣列
     */
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
    
    /**
     * @param 陣列a
     * @return 將ArrayList裡元素賦值到陣列中
     * @throws NullPointerException a為null
     * @throws ArrayStoreException a的執行時型別不是ArrayList中每個元素的執行時型別的超型別
     */
    public  T[] toArray(T[] a) {
        if (a.length < size)
            // 建立一個新的a的執行時型別陣列,內容不變
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }
複製程式碼

序列化

我們知道ArrayList實現了Serializable,其elementData是一個快取陣列,其容量少數情況會滿,只需要序列化實際儲存的元素即可,所以用transient修飾標記不可序列化.ArrayList通過獨特方式完成序列化,在序列化時會呼叫定義的writeObject,將size和element寫入物件輸出流中.


    /**
     * 序列化
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        int expectedModCount = modCount;
        //預設序列化方法,將當前類的非靜態和非transient欄位寫入流中
        s.defaultWriteObject();
        // 寫入大小
        s.writeInt(size);
        // for迴圈寫入ArrayList中所有元素
        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;
        // 預設反序列化方法
        s.defaultReadObject();
        // 讀取長度
        s.readInt(); // ignored
        if (size > 0) {
            // 像clone()方法 ,根據大小而不是容量分配陣列
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);
            Object[] a = elementData;
            //讀取所有元素
            for (int i=0; i < size; i++) {
                a[i] = s.readObject();
            }
        }
    } 
複製程式碼

遍歷

在簡述中提到了ArrayList實現了RandomAccess介面,其有一段英文註釋


     As a rule of thumb, a List implementation should 
     implement this interface if,
 for typical instances of the class, this loop:
     for (int i=0, n=list.size(); i < n; i++)
          list.get(i);
 runs faster than this loop:
      for (Iterator i=list.iterator(); i.hasNext(); )
          i.next();
複製程式碼

for迴圈遍歷效率高過foreach,在Collections工具類中個方法:


    private static final int FILL_THRESHOLD           =   25;

    public static  void fill(List list, T obj) {
        int size = list.size();
        if (size < FILL_THRESHOLD || list instanceof RandomAccess) {
            for (int i=0; i itr = list.listIterator();
            for (int i=0; i < size; i++) {
                itr.next();
                itr.set(obj);
            }
        }
    }
複製程式碼

我們可以從這個方法可以看出若是實現了RandomAccess用了for迴圈遍歷.

ArrayList的iterator與listIterator區別

iterator與listIterator都是迭代器,不同的是listIterator可以指定位置開始,擁有hasPrevious()和previous()方法能逆向遍歷,可以對集合本身進行新增,修改不過必須要next()過且不能連續操作,還可以返回當前遍歷位置nextIndex(),previousIndex()當前遍歷位置前一個.

手撕簡單實現


public class ArrayList {

    private Object[] elementData;

    private int size;

    public ArrayList(int capacity){
        this.elementData = new Object[capacity];
    }

    public ArrayList(){
        this.elementData = new Object[10];
    }

    public void add(Object obj){
        isExpanse(size + 1);
        elementData[size++] = obj;
    }

    private void isExpanse(int minCapacity){
        int oldCapacity = elementData.length;
        if(minCapacity > oldCapacity){
            int newCapacity = oldCapacity + oldCapacity >> 1;
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    }

    public Object get(int index){
        checkLength(index);
        return elementData[index];
    }

    public void set(int index, Object obj){
        checkLength(index);
        elementData[index] = obj;
    }

    private void checkLength(int index){
        if(index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("越界");
        }
    }

    public void remove(int index){
        checkLength(index);
        int moveNum = size - index - 1;
        if(moveNum > 0 ){
            System.arraycopy(elementData, index + 1, elementData, index, moveNum);
        }
        elementData[size--] = null;
    }
}
複製程式碼

相關文章