掌握這些,ArrayList就不用擔憂了!

關於ArrayList的學習

ArrayList屬於Java基礎知識,面試中會常常問到,因此做爲一個Java從業者,它是你不得不掌握的一個知識點。😎java

可能不少人也不知道本身學過多少遍ArrayList,以及看過多少相關的文章了,可是大部分人都是當時以爲本身會了,過不了多久又忘了,真的到了面試的時候,本身回答的支支吾吾,本身都不滿意😥c++

爲何會這樣?對於ArrayList這樣的知識點的學習,不要靠死記硬背,你要作的是真的理解它!😁面試

我這裏建議,若是你真的想清楚的理解ArrayList的話,能夠從它的構造函數開始,一步步的讀源碼,最起碼你要搞清楚add這個操做,記住,是源碼😄算法

一個問題看看你對ArrayList掌握多少

不少人已經學習過ArrayList了,讀過源碼的也很多,這裏給出一個問題,你們能夠看看,以便測試下本身對ArrayLIst是否真的掌握:數組

請問在ArrayList源碼中DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA是什麼?它們有什麼區別?安全

怎麼樣?若是你能很輕鬆的回答上來,那麼你掌握的不錯,不想再看本篇文章能夠直接出門右拐(我也不知道到哪),若是你以爲不是很清楚,那就跟着我繼續往下,我們再來把ArrayList中那些重點過一遍!😎數據結構

你以爲ArrayList的重點是啥?

在我看來,ArrayList的一個至關重要的點就是數組擴容技術,咱們以前學習過數組,想一下數組是個什麼玩意以及它有啥特色。併發

隨機訪問,連續內存分佈等等,這些學過的都知道,這裏說一個彷佛很容易被忽略的點,那就是數組的刪除,想一下,數組怎麼作刪除?😏函數

關於數組刪除的一些思考

關於數組的刪除,我以前也是有疑惑,後來也花時間思考了一番,算是比較通透了,這裏就提一點,數組並無提供刪除元素的方法,咱們都是怎麼作刪除的?工具

好比咱們要刪除中間的一個元素,怎麼操做,首先咱們能夠把這個元素置爲null,也就把這個元素刪除掉了,此時數組上就空出了一個位置,這樣行嗎?

當咱們再次遍歷這個數組的時候是否是仍是會遍歷到這個位置,那麼就會報空指針異常,怎麼辦?是的咱們能夠先判斷,可是這樣的作法很差,怎麼辦呢?

那就是咱們能夠把這個元素後面的全部元素統一的向前複製,有的地方這裏會說移動,我以爲不夠合理,爲啥?

複製是把一個元素拷貝一份放到其餘位置,原來位置元素還存在,而移動呢?區別就是移動了,本來的元素就不存在了,而數組這裏是複製,把元素統一的各自向前複製,最終結果就是倒數第一和第二位置上的元素是相同的。

此時的刪除的本質其實是要刪除的這個元素的後一個元素把要刪除的這個元素給覆蓋了,後面依次都是這樣的操做,可能有點繞,本身想一下。

因此就引出了數組的刪除操做是要進行數組元素的複製操做,也就致使數組刪除操做最壞的時間複雜度是0(n)。

爲何說這個?由於對理解數組擴容技術頗有幫助!

數組擴容技術

上面咱們談到了關於數組的刪除操做,咱們只是分析了該如何去刪除,可是數組並未提供這樣的方法,若是咱們要搞個數組,這個刪除操做仍是要咱們本身寫代碼去實現的。

不過好在已經有實現了,誰嘞,就是咱們今天的主角ArrayList,其實ArrayList就能夠看做是數組的一個升級版,ArrayList底層也是使用數組來實現,而後加上了不少操做數組的方法,好比咱們上面分析的刪除操做,固然除此以外,還實現了一些其餘的方法,而後這就造成了一個新的物種,這就是ArrayList。

本質上ArrayList就是一個普通的類,對數組進行的封裝,擴展其功能

對於數組,咱們還了解一點那就是數組一旦肯定就不能再被改變,而這個ArrayList卻能夠實現自動擴容,有木有以爲很高級,其實也沒啥,由於數組自己特性決定,ArrayList所謂的自動擴容其實也是新建立一個數組而已,由於ArrayList底層就是使用的數組。

咱們的重點須要關注的是這個自動擴容的過程,就是怎麼建立一個新的數組,建立完成以後又是怎麼作的,這纔是咱們關注的重點。

接下來咱們看兩種數組擴容方式。

Arrays.copyof

不知道你使用過沒,咱們直接看代碼:

public static void main(String[] args) {
        int[] a1 = new int[]{1, 2};
        for (int a : a1) {
            System.out.println(a);
        }
        System.out.println("-------------拷貝------------");
        int[] b1 = Arrays.copyOf(a1, 10);
        for (int b : b1) {
            System.out.println(b);
        }
    }

代碼很少,很簡單,看看輸出結果你就明白了

在這裏插入圖片描述
ok,是否是很簡單,知道這個簡單用法就ok了,接下來看另一種

System.arraycopy()

這個方法咱們看看是個啥:

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

看見沒,native修飾的,通常是使用c/c++寫的,性能很高,咱們看看這裏面的這幾個參數都是啥意思:

src:要拷貝的數組
srcPos:要拷貝的數組的起始位置
dest:目標數組
destPos:目標數組的起始位置
length:你要拷貝多少個數據

怎麼樣,知道這幾個參數什麼意思了,那使用就簡單了,我這裏就不顯示了。

ps:之後複製數組別再傻傻的遍歷了,用這個多香😄

以上兩個方法都是進行數組拷貝的,這個對理解數組擴容技術很重要,並且在ArrayList中也有應用,咱們等會會詳細說。

下面我們開始看看ArrayList的一些源碼,加深咱們對ArrayList的理解!

源碼中的ArrayList

通常咱們是怎麼用ArrayList的呢?看下面這些代碼:

ArrayList arrayList = new ArrayList();
        arrayList.add("hello");
        arrayList.add(1);

        ArrayList<String> stringArrayList = new ArrayList<>();
        stringArrayList.add("hello");

簡單,都會吧,就是new一個出來,不過上面的代碼我還想說明一個問題,當你不指定具體類型的時候是能夠存儲任意類型的數據的,指定的話就只能存儲特定類型,爲啥不指定能夠存儲任意類型?

這個問題不作解釋,等會看源碼你就明白了。

看看ArrayList的無參構造函數

通常咱們看ArrayList的源碼,都是從它的無參構造函數開始看起的,也就是這個:

new ArrayList();

好啦,走進去看看這個new ArrayList();構造函數長啥樣吧。

/**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

咋一看,代碼很少,簡單,裏面就是個賦值操做啊,有兩個新東西elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA,這是啥?🤪

不着急,咱們點進去看看

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access

這不就是Object數組嘛,好像還真是的,那transient啥意思?它啊,你就記住被它修飾序列化的時候會被忽略掉。

好了,除此以外,就是個數組,對Object類型的。

很差像有點區別啊,DEFAULTCAPACITY_EMPTY_ELEMENTDATA已經指定是個空數組了,而elementData只是聲明,在new一個ArrayList的時候進行了賦值,也就是這樣:

this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

咋樣?明白了吧,以前不就說了嘛,ArrayList底層就是一個數組的,這裏你看,new以後不就給你弄個空數組出來嘛,也就是說啊,你要使用ArrayList,一開始先new一下,而後給你搞個空數組出來。

啥?空數組?空數組怎麼行呢?畢竟咱們還須要用它存數據嘞,因此啊,重點來了,咱們看它的add,也就是添加數據的操做。

看看ArrayList的add

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

就是這個啦,ArrayList不就是使用add來添加數據嘛,咱們看看是怎麼操做的,咋一看這段代碼,讓咱們感到比較陌生的就是這個方法了

ensureCapacityInternal(size + 1);

這是啥玩意,翻譯一下😂

在這裏插入圖片描述
確保內部容量?什麼鬼,這裏還有個size,咱們看看是啥?

private int size;

就是一個變量啊,咱們再看看這段代碼

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

尤爲是

elementData[size++] = e;

知道了嘛?咱們以前不是已經建立了一個空數組,不就是elementData嘛,這好像是在往數組裏面放數據啊,不過不對啊,不是空數組嘛?咋能放數據,這不是前面還有這一步嘛

ensureCapacityInternal(size + 1);

是否是有想法了,這一步應該就是把數組的容量給肯定下來的,趕忙進去看看

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

就是這個了,這一步很重要:

if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

也好理解吧,就是先判斷下如今這個ArrayList的底層數組elementData 是否是剛建立的的空數組,這裏確定是啊,而後開始執行

minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

minCapacity是個啥(重要)

說這個以前,你先得搞清楚這個minCapacity 是啥,它如今其實就是底層數組將要添加的第幾個元素,看看上一步

ensureCapacityInternal(size + 1);

這裏size+1了,因此如今minCapacity 至關因而1,也就是說將要向底層數組添加第一個元素,這一點的理解很重要,因此從minCapacity 的字面意思理解也就是「最小容量」,我如今將要添加第一個元素,那你至少給我保證底層數組有一個空位置,否則怎麼放數據嘞。

重點來了,由於第一次添加,底層數組沒有一個位置,因此須要先肯定下來一共有多少個位置,就是獻給數組一個默認的長度

因而這裏給從新賦值了(只有第一次添加數據纔會執行這步,這一步就是爲了指定默認數組長度的,指定一次就ok了)

minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

這怎麼賦值的應該知道嘛,哪一個大取哪一個,那咱們要看看DEFAULT_CAPACITY是多少了

/**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

ok,明白了,這就是ArrayList的底層數組elementData初始化容量啊,是10,記住了哦,那麼如今minCapacity就是10了,咱們再接着看下面的代碼,也便是:

ensureExplicitCapacity(minCapacity);

進去看看吧:

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

也比較簡單,如今底層數組長度確定還不到10啊,因此咱們繼續看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);
    }

咋一看,判斷很多啊,幹啥的都是,忽然看到了Arrays.copyOf,知道這是啥吧,上面但是特地講過的,原來這是要進行數組拷貝啊,那這個elementData就是原來的數組,newCapacity就是新數組的容量

咱們一步步來看代碼,首先是

int oldCapacity = elementData.length;

獲得原來數組的容量,接着下一步:

int newCapacity = oldCapacity + (oldCapacity >> 1);

這是獲得新容量的啊,不事後面的這個oldCapacity >> 1有點看不懂啊,其實這oldCapacity >> 1就至關於oldCapacity /2,這是移位運算,感興趣的自行搜索學習。

知道了,也就是擴容爲原來的1.5倍,接下來這一步:

if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;

由於目前數組長度爲0,因此這個新的容量也是0,而minCapacity 則是10,因此會執行方法體內的賦值操做,也就是如今的新容量成了10。

接着這句代碼就知道怎麼回事了

elementData = Arrays.copyOf(elementData, newCapacity);

不知道你發現沒,這裏饒了一大圈,就是爲了建立一個默認長度爲10的底層數組。

底層數組長度要看ensureCapacityInternal

ensureCapacityInternal這個方法就像個守衛,時刻監視着數組容量,而後過來一個數值,也就是說要向數組添加第幾個數據,那ensureCapacityInternal須要思考思考了,思考啥呢?固然是看底層數組有沒有這麼大容量啊,好比你要添加第11個元素了,那底層數組長度最少也得是11啊,否則添加不了啊,看它是怎麼把關的

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

記住了這段代碼

if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

它的存在就是爲了一開始建立默認長度爲10的數組的,當添加了一個數據以後就不會再執行這個方法,因此重難點是這個方法:

ensureExplicitCapacity(minCapacity);

也就是真正的把關在這裏,看它的實現:

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

怎麼樣,看明白了吧,好比你要添加第11個元素,但是個人底層數組長度只有10,不夠啊,而後執行grow方法,幹嗎執行這個方法,它其實就是用來擴容的,不信你再看看它的實現,上面已經分析過了,這裏就不說了。

假如你要添加第二個元素,這裏底層數組長度爲10,就不須要執行grow方法,由於根本不須要擴容啊,因此這一步實際啥也沒作(有個計數操做):
在這裏插入圖片描述
而後就直接在相應位置賦值了。

小結

因此這裏很重要的一點就是理解這一步傳入的值的意義:

ensureCapacityInternal(size + 1);

簡單點就是要向底層數組中添加第幾個元素了,而後開始進行一系列的判斷,容量夠的話直接返回,直接賦值,不夠的話就執行grow方法開始擴容。

主要判斷就在這裏:

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

具體的擴容是這裏

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);
    }

這裏須要注意這段代碼

if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;

這段代碼只有在第一次添加數據的時候纔會執行,也是爲建立默認長度爲10的數組作準備的,由於這個時候本來數組長度爲0,擴容後也是0,而minCapacity 爲默認值10,因此會執行這段代碼。

可是一旦添加數據以後,底層數組默認就是10了,再加上以前的判斷,這裏的newCapacity 必定會比minCapacity 大,這個點須要瞭解。

看看ArrayList的有參構造函數

咱們上面着重分析了下ArrayList的無參構造函數,下面再來看看它的有參構造函數:

ArrayList arrayList1 = new ArrayList(100);

看看這個構造函數張啥樣?

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);
        }
    }

我去,這不就是直接建立嘛,而後還有這個:

else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        }

咱們看看這個EMPTY_ELEMENTDATA

private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

ok,如今你能夠回答咱們開篇提的那個問題了吧。

咱們以上對ArrayList的源碼有了必定的認識以後,咱們再來看看ArrayList的讀取,替換和刪除操做時怎樣的?

ArrayList的其餘操做

通過上面的分析,我相信你對ArrayList的其餘諸如讀取刪除等操做也沒啥問題,一塊兒來看下。

讀取操做

看源碼

public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

代碼很簡單,rangeCheck就是用來判斷數組是否越界的,而後直接返回下標對應的值。

刪除操做

看源碼

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 E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

其實就是把原來的值覆蓋,沒啥問題吧😄

和vector很像

這個想必你們都知道,ArrayList和vector是很像的,前者是線程不安全,後者是線程安全,咱們看一下vector一段源碼就明白了

public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

沒錯,區別就是這麼明顯!

總結

到這裏,咱們基本上把ArrayList的相關重點都過了一遍,對於ArrayList來講,重點就是分析它的無參構造函數的執行,通過分析,咱們知道了它有個數組拷貝的操做,這塊是會影響到它的一些操做的時間複雜度的,關於這點,就留給你們取思考吧!

好了,今天就到這裏,你們若是有什麼問題,歡迎留言,一塊兒交流學習!

感謝閱讀

你們好,我是ithuangqing,一路走來積累了很多的學習經驗和方法,並且收集了大量的精品學習資源,如今維護了一個公衆號【編碼以外】,寓意就是在編碼以外也要不停的學習,主要分享java技術相關的原創文章,如今主要在寫數據結構與算法,計算機基礎,線程和併發以及虛擬機這塊的原創,另外針對小白還在連載一套《小白的java自學課》,力求通俗易懂,由淺入深。同時我也是個工具控,常常分享一些高效率的黑科技工具及網站

對了,公衆號還分享了不少個人學習心得,能夠一塊兒探討探討!

關注公衆號,後臺回覆「慶哥」,2019最新java自學資源立馬送上!更多原創精彩盡在【編碼以外】

在這裏插入圖片描述

相關文章
相關標籤/搜索