ArrayList原始碼閱讀,拒絕人云亦云~

小白大菜發表於2020-12-03

ArrayList我們稱之為動態陣列,其在保留了傳統陣列快速查詢優點的同時,也增加了可變長這一特性。

在Java的世界中,陣列可以說是我們用的最多的資料結構之一。陣列是什麼呢?個人理解就是記憶體中一塊連續的儲存相同型別元素的地方(這個“地方”用的比較low。。。多看書多看書)
陣列的優點是什麼呢?查詢快呀!!!
大家都知道查詢快~
為什麼查詢快呀?有索引呀!!!
哦?那索引又是什麼?陣列下標啊,0,1,2,3…
原來索引就是0,1,2,3呀~
那有索引為啥就快呀?
額。。。年輕人不講武德,耗子尾汁!!!
我大意了,沒有說清楚
為什麼有索引就會查詢快呢?對於陣列我們實際上在記憶體中存的是陣列元素的首地址,且陣列儲存的元素是地址連續的,因此當我們想要查詢某個索引對應的值時,只需要用首地址+index*元素大小(如byte,short,int,long,分別為1,2,4,8),就是對應索引的地址啦,時間複雜度為O(1)
是不是很快?什麼?你沒感覺到快?
那給你舉個查詢慢的資料結構,連結串列。連結串列連結串列,顧名思義,它是鏈起來的,每個連結串列的節點都會儲存與之相鏈的下一個節點的地址(單向連結串列),因此他們的地址幾乎是不連續的,因此當我們想要查詢某個節點時,不管怎樣,我們每次都得從頭節點開始遍歷,直到找到我們想要的那個元素(如果按照一般回答,有索引就快,那連結串列也是有索引的。。。),因此連結串列查詢的時間複雜度就是O(n)。
那陣列的缺點是啥嘞?普通陣列的缺點就是不可變長,這個問題就會造成記憶體浪費,或者記憶體不夠,這是一個尷尬的問題。。。
因此動態陣列來了,動態陣列繼承了傳統陣列查詢快這一優點同時又可變長,那麼動態陣列的缺點是啥呢?
增刪慢呀。
為啥增刪慢呀?
因為動態陣列在每一次增刪,都會在整個陣列上操作,有點牽一髮而動全身的趕腳,與之相反連結串列在增刪比較快,因為連結串列只需要斷鏈再連線鏈即可,只是區域性地方在變化。
什麼?你不信?來我們看原始碼!
直接奔向add方法

    //增加一個元素,只要不報錯,返回為恆為true
    public boolean add(E e) {
        //擴容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //末尾插入元素
        elementData[size++] = e;
        return true;
    }

    //在指定index處插入一個值
    public void add(int index, E element) {
        //檢查index合法性
        rangeCheckForAdd(index);

   		//擴容,注意並不是容量擴充為size+1,請細看grow函式
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //將原陣列的index位置及以後的元素複製給從index+1開始及以後的位置
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //將指定元素放到index位置
        elementData[index] = element;
        //容量+1
        size++;
    }

我們看到ArrayList裡面有兩個add方法,順便複習一下這叫做方法過載(與之相對就是重寫)
這兩個add方法,分別是在末尾插入一個元素以及在指定位置插入一個元素,先看第一個
,首先映入眼簾的是ensureCapacityInternal()選手

    //若傳入陣列為空則返回初始容量與傳入引數之間的最大值
    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++,代表這個ArrayList被修改一次(這個與迭代器有關)
        modCount++;

        // overflow-conscious code
        //若傳入引數比陣列本身的容量大,則進行grow
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

走我們去看看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);
    }
	
    private static int hugeCapacity(int minCapacity) {
        //若傳入引數小於0,丟擲OutOfMemoryError
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //若傳入引數大於最大容量則返回Integer的最大值,否則返回最大容量
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

我們繼續看一下Arrays.copyOf()

public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }

繼續往裡面走copyOf()

//引數分別是原陣列,新的陣列長度,以及陣列型別
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        //先判斷陣列型別是否是Object型,反正最後都是new了一個新的陣列
        T[] copy = ((Object)newType == (Object)Object[].class)
        	//若是則直接new一個Object型陣列
            ? (T[]) new Object[newLength]
            //若不是則通過反射建立一個對應型別陣列
            //getComponentType:返回Class物件,如果不是Class物件那麼返回null
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        //然後將原陣列複製到新的copy陣列上,複製的起始是從0開始
        //到original.length與newLength之間最小長度的值
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        //然後返回新的陣列
        return copy;
    }

什麼?還想看System.arraycopy()?走起

public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

一看是native修飾。。。惹不起惹不起,溜了溜了
我們接著看另外一個add方法,道理是一樣的啦,註釋寫的比較清楚~

走,我們去看remove方法,同樣寫在了註釋了

    //移除指定索引位置元素
    public E remove(int index) {
        //檢查index合法性
        rangeCheck(index);

        //記錄ArrayList被修改過一次
        modCount++;
        //要被移除的值
        E oldValue = elementData(index);

        //要複製的元素個數
        int numMoved = size - index - 1;
        if (numMoved > 0)
            //將原陣列index+1及以後的元素複製到原陣列index及以後的位置上,複製長度為numMoved 
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //將最後一個位置設定為空,意思是GC會最後處理這個空閒地址?
        elementData[--size] = null; // clear to let GC do its work
        //返回被移除的值
        return oldValue;
    }


    //移除指定元素
    public boolean remove(Object o) {
        //若指定元素為空
        if (o == null) {
            //找到第一個為指定元素的就移除並返回true
            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;
                }
        }
        //若沒找到則返回false
        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
    }

ArrayList裡面還有許多其他的方法。。。你們繼續看~

相關文章