ArrayList集合深度解析

一. 手寫高仿ArrayList集合

基本原理思想

Arraylist集合底層使用動態數組實現,隨機查詢效率非常快,插入和刪除需要移動整個數組、效率低。

1. 高仿ArrayList集合

public interface MyList<E> {
    /**
     * 集合的大小(長度)
     * @return
     */
    int size();
    /**
     * 往集合中添加我們的元素
     * @param e
     * @return
     */
    boolean add(E e);
    /**
     * 使用下標查詢到我們的集合元素
     * @param index
     * @return
     */
    E get(int index);
    /**
     * 使用下標位置刪除我們的元素
     * @param index
     * @return
     */
    public E remove(int index);
}
public class MyArraylist<E> implements MyList<E> {
    /**
     * elementData數據存放我們Arraylist所有的數據 transient作用不能序列化
     */
    transient Object[] elementData;
    /**
     * 給我們的數組容量賦值爲空
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    /*
     * 數組的容量默認大小爲0
     */
    private int size;
    /**
     * 數組默認容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;
    /**
     * 2的31次方-1 -8
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    public MayiktArraylist() {
        // 給我們的數組容量賦值爲空
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean add(E e) {
        // 對我們的數組實現【擴容】
        ensureCapacityInternal(size + 1);
        // 對我們的數據元素【賦值】
        elementData[size++] = e;
        return true;
    }

    @Override
    public E get(int index) {
        rangeCheck(index);
        // 根據下標從數組中查詢到數據
        return (E) elementData[index];
    }

    @Override
    public E remove(int index) {
        // 檢查我們的下標位置是否越界
        rangeCheck(index);
        // 獲取要刪除的對象
        E oldValue = get(index);
        //計算移動的位置
        int numMoved = size - index - 1;
        // 判斷如果刪除數據的時候 不是最後一個的情況下,將刪除後面的數據往前移動一位
        if (numMoved > 0) {
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);
        }
        // 如果numMoved 爲0的情況下,說明後面不需要往前移動,直接將最後一條數據賦值爲null
        elementData[--size] = null; // clear to let GC do its work
        return oldValue ;
    }

    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException("下標位置越界啦index:" + index);
    }

    private void ensureCapacityInternal(int minCapacity) {
        // 添加元素的時候 如果我們數組是爲空的情況下
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //DEFAULT_CAPACITY =10;   minCapacity=0+1  DEFAULT_CAPACITY=10 10>1
            //minCapacity=10;
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

    // modCount++; 增刪改的時候  modCount++
    private void ensureExplicitCapacity(int minCapacity) {
        // 10- 數組長度 (0) 10-0 >0  作用:判斷我們數組中是否需要繼續擴容
        if (minCapacity - elementData.length > 0) {
            // 對我們的數組實現擴容
            grow(minCapacity);
        }
    }

    // length 和size區別
    private void grow(int minCapacity) {
        // 獲取我們的數組的長度 old原來的 new新的 原來數組容量 0;
        int oldCapacity = elementData.length;
        // 新的容量 = 原來的容量 + 原來的容量/2 = 0
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 新的容量(0)-最小的容量(10) < 0  -10
        if (newCapacity - minCapacity < 0) {
            // 新的容量=10 作用:第一次對我們數組做初始化容量操作
            newCapacity = minCapacity;
        }
        // 判斷我們擴容長度大於Integer 21 最大值的情況下
        // 限制我們數組擴容最大值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 開始對我們的數組實現擴容 對我們的數據實現擴容(newCapacity) 將舊的數組數據複製到新的數組中,並可以指定長度
        elementData = Arrays.copyOf(elementData, newCapacity);//這裏無非就是把相同的數據複製了一份,並重新指定了數組的length
    }

    /**
     * 判斷我們最小的初始化容量
     * @param minCapacity minCapacity==最小的容量>Integer 21 最大值的情況下  Integer.MAX_VALUE
     *                    minCapacity 相當於當前添加元素的下標位置
     * @return
     */
    private static int hugeCapacity(int minCapacity) {
        /*
            Integer.MAX_VALUE : 2147483647
            Integer.MAX_VALUE + 1 :-2147483648
            只要大於IntegerMAX_VALUE,就會變爲負數,所以這裏判斷是否小於0
         */
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }
}
public class Test {
    public static void main(String[] args) {

        MyArraylist<String> list= new MyArraylist<String>();

        for (int i = 0; i < 10; i++) {
            list.add("元素" + i);
        }
        System.out.println(list.size());

        list.add("元素11");

        System.out.println("刪除數據之前:");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
//        // 下一次數組擴容是在什麼時候呢?
        list.remove(2);
        System.out.println("刪除數據之後:");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
//        // 數組的下標爲0 開始 刪除第一條數據 10=11
//        System.out.println(list.get(10));

    }
}

小結:第一次add的時候,會擴容/初始化 數組length爲10,下一次擴容是在什麼時候?

答案:添加第11條數據的時候【原理見ensureExplicitCapacity()方法】

添加第11條數據的時候,length會擴容到多少呢?
答案:15 【原理見grow()方法】
                 // 新的容量 = 原來的容量 + 原來的容量/2 = 10 + 10/2 = 15
                int newCapacity = oldCapacity + (oldCapacity >> 1);
以此類推,添加第16條的時候,會擴容到16+16/2=24

數組最大容量就是Integer.MAX_VALUE

2. 仔細看ArrayList的源碼,不難發現,每次add和remove的時候,都會調用全局變量modCount++

爲什麼這麼做呢,此時我們先引入一個理論:Fail-Fast機制原理

Fail-Fast是我們Java集合框架爲了解決集合中結構發生改變的時候,快速失敗的機制。

舉例說明:

package com.example;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Test004 {

    // 定義一個全局的集合存放 存在在高併發的情況下線程安全的問題
    private List<String> strings = new ArrayList<String>();
//    private List<String> strings = new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        new Test004().startRun();
    }

    public void startRun() {
        new Thread(new ThreadOne()).start();
        new Thread(new ThreadTWo()).start();
    }

    // 打印我們的數據
    private void print() {
        strings.forEach((t) -> System.out.println("t:" + t));
    }

    class ThreadOne implements Runnable {
        @Override
        public void run() {
            // 存儲數據
            for (int i = 0; i < 10; i++) {
                strings.add("i:" + i);
                print(); // 打印數據
            }
        }
    }

    class ThreadTWo implements Runnable {
        @Override
        public void run() {
            for (int i = 10; i < 20; i++) {
                strings.add("i:" + i);
                print();
            }
        }
    }
}

執行main方法,會報錯:ConcurrentModificationException即併發修改異常

那麼我們定位到ArrayList的第1252行:

即我們調用下圖的時候,會走到上圖ArrayList源碼(用for each的時候,底層還是用for循環去遍歷)

for each原理:會把modCount賦值給一個臨時變量/期望變量expectedModCount,for循環的時候,判斷modCount的值是否發生變化,如果沒發生變化,纔會進行打印值等操作,如果發生變化,則會拋出併發修改異常。

爲什麼會拋出異常:modCount是全局的,可能會被add,remove進行更改,而我們的臨時變量expectedModCount是局部的,不會更改。線程一和線程二可能會同時調用add方法,比如線程一調用完add,在打印之前,線程二調用add方法(modCount++),此時會導致modCount和expectedModCount不一致,則拋出併發修改異常。

解決上面Fail-Fast場景,可以使用CopyOnWriteArrayList,該集合是,添加和修改的時候都加了Lock鎖。

CopyOnWriteArrayList特點:

  • 內部持有一個ReentrantLock lock = new ReentrantLock();
  • 底層是用volatile transient聲明的數組 array
  • 讀寫分離,寫時複製出一個新的數組,完成插入、修改或者移除操作後將新數組賦值給array

3. ArrayList刪除原理

 這裏我們重點分析System.arraycopy方法,首先貼出ArrayList的remove方法

System提供了一個靜態方法arraycopy(),我們可以使用它來實現數組之間的複製,其函數原型是:

 public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) ;參數解析如下:

src 源數組
srcPos 源數組要複製的起始位置
dest 目的數組
destPos 目的數組放置的起始位置
length 複製的長度

注意:src 和 dest都必須是同類型或者可以進行轉換類型的數組。下面舉例講解:

package com.example;

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

        Object[] objects = new Object[]{"0", "1", "2", "3"};

        // 要刪除元素的索引,也就是目的數組放置的起始位置
        int index = 0;
        // 源數組要複製的起始位置
        int destPos = index + 1;
        // 要複製的長度
        int numMoved = objects.length - index - 1;

        System.arraycopy(objects, destPos, objects, index, numMoved);
        objects[objects.length - 1] = null;
        for (int i = 0; i < objects.length; i++) {
            System.out.println(objects[i]);
        }

    }
}

運行結果爲:

那麼疑問來了,我都刪了,爲什麼還有四個元素?

答案:這是數組的長度爲4,在實際ArrayList刪除的時候,會發現,即集合的size會減1,但是數組的容量還是沒變的,這點不要混淆了~  。如果要刪除1,即前面的元素不會影響,只移動後面的,並把最後一個置爲null。


下面貼出一段代碼:

public class AAA {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        System.out.println(list.size());

        list.remove(3);
        System.out.println(list.size());
        System.out.println(list.get(4));
}

執行結果爲:

說好的刪了會置爲null呢?原因是ArrayList源碼中調用get()方法時有個下標監測判斷~

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException("下標位置越界啦index:" + index);
    }
}

ArrayList的remove方法核心就是使用了System的靜態方法arraycopy( );再次貼出我們手寫的ArrayList源碼:

可以發現,會先判斷numMoved,如果numMoved大於0,證明要刪除的不是最後一個元素,則執行System.arraycopy方法進行數組複製;複製完成後,會執行 elementData[--size] = null; 將最後一個元素置爲null。並且size減1。(--size代表先執行size=size-1,然後再使用size的值,所以將數組的第size-1個元素,也就是最後一個元素置爲null【注意索引從0開始】)

二. ArrayList,Vector,CopyOnWriteArrayList 對比

1. ArrayList與Vector的區別

說到這裏,我們又會想到一個集合Vector,該集合是ArrayList的前身,相信有一定基礎的小夥伴都瞭解過該集合。

這裏我結合源碼,列舉一下ArrayList與Vector的區別:

相同點:底層都是採用數組實現

不同點:

① 默認初始化時候

Arraylist 默認 不會對我們數組做初始化

(第一次調用add方法的時候 纔會初始化)

Vector 默認初始化的大小爲10

② 擴容區別

ArrayList擴容:在原來數組的基礎之上增加50%,即新容量是舊容量的1.5倍

Vector擴容:在原來數組基礎之上再增加100%,即新容量是舊容量的2倍(如果沒自定義容量capacityIncrement),所以新容量newCapacity = oldCapacity + oldCapacity;

那麼如何自定義capacityIncrement,直接調用如下Vector的方法:

③ 線程是否安全(主要看增刪,查的時候不存在線程安全問題)

ArrayList  默認的情況下 線程不安全的,因爲沒加線程同步,沒加鎖。

Vector 線程是安全的 效率是非常低的 查詢 增加 、刪除都加上了鎖。

2. CopyOnWriteArrayList和Vector區別:

Vector:讀,寫,查都加上了synchronized同步鎖(不建議使用)

CopyOnWriteArrayList:寫,刪的時候加了Lock鎖,但讀的時候沒加鎖;CopyOnWriteArrayList支持讀多寫少的併發情況

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章