【Java集合篇】對比java7和8深度解析ArrayList(只要看,就能懂)

前言:

如果作爲java開發工程師,大家都使用過List的主要實現類ArrayList,那麼你真的懂他嗎?

  • 1.ArrayList是單列的、存儲有序的、可重複的、線程不安全的,但是你知道爲什麼嗎?

  • 2.是不是感覺ArrayList與數組非常相似? 那麼爲什麼ArrayList不會下標越界呢?那ArrayList底層做了什麼呢?

  • 3.如題:從java7到java8進行了哪些迭代?爲什麼要這樣做?

    • java8的ArrayList是什麼樣子的?
    • java7的ArrayList是什麼樣子的?
  • 4.如何高效的使用ArrayList?

環境介紹

因爲本身詳細介紹與對比了java7和java8的ArrayList的源碼,在此聲明本文的環境:

  • JDK7(以jdk-7u7-windows-x64爲例):後續文章簡稱爲java8
  • JDK8(以jdk-8u131-windows-x64爲例):後續文章中簡稱java7

問題一:ArrayList是單列的、存儲有序的、可重複的、線程不安全的,但是你知道爲什麼嗎?

1.爲什麼是單列的?

下面一段代碼是小編從java中ArrayList的源碼中截取的(這裏java7和java8不存在差異)elementData這個成員變量的作用是對ArrayList的添加的元素進行存儲,可以看出這是一個Object類型的數組,因爲數組是線程存儲的,所以ArrayList也是線性存儲的。

    /**
     * 存儲ArrayList元素的數組緩衝區。ArrayList的容量是這個數組緩衝區的長度。任何帶有
     * elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList將在添加第一
     * 個元素時擴展爲DEFAULT_CAPACITY。
     */
    transient Object[] elementData; // non-private to simplify nested class access

2.爲什麼有序的?

從上面可以看出,ArrayList底層是Object類型的數組進行存儲,數組是有序的,所以其存儲也是有序的,那麼接下來,咱們進行驗證一下:

    @Test
    public void test()
    {
        ArrayList list=new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println("list的輸出結果:"+list);
    }

在這裏插入圖片描述
根據結論可以看出,ArrayList的確是順序存儲的。

3.爲什麼是可重複的?

閒話少說,來個例子看看:

    @Test
    public void test()
    {
        ArrayList list=new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(3);
        System.out.println("list的輸出結果:"+list);
    }

在這裏插入圖片描述
正如輸出結果:兩個完全相同的,所以ArrayList是可以存儲可重複的數據的,接下來就看看源碼:
源碼分析:以java8爲例

  • 1.首先數組是可以存儲重複的數據的,
  • 2.如果ArrayList在添加的時候,沒有進行過濾,那麼他就是可以存儲重複的數據。
	public boolean add(E e) {
		//size爲添加前數據元素的個數,加一以後,代表加上新添加的元素的長度
		//該方法主要作用是:判斷是否開闢了空間,判斷是否擴容,此處省略,問題2會詳細講述
        ensureCapacityInternal(size + 1); 
        //將elementData的下一份元素賦值爲e
        elementData[size++] = e;
        //返回true
        return true;
    }
    //minCapacity長度:爲ArrayList添加新元素後的長度。
    private void ensureCapacityInternal(int minCapacity) {
    	//這裏暫時略過,後續在java7和java8的時候會講
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
		//帶上minCapacity形參,去下面的方法
        ensureExplicitCapacity(minCapacity);
    }
    
    //minCapacity長度:爲ArrayList添加新元素後的長度。
    private void ensureExplicitCapacity(int minCapacity) {
    	//這是一個快速失敗機制,此處略過
        modCount++;
		//判斷數組長度,如果不夠就擴容,此處省略後續會講
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);  
    }

4.爲什麼是線程不安全的?

有理有據,看源碼
正如下面的圖是筆者隨意截取的一張圖,發現方法中並沒有synchronized關鍵字,所以是線程不安全的。
在這裏插入圖片描述

問題二:是不是感覺ArrayList與數組非常相似?那麼爲什麼ArrayList不會下標越界呢?那ArrayList底層做了什麼呢?

1.是不是感覺ArrayList與數組非常相似?

這個問題:相信大家都瞭解了,因爲在前面已經看到了ArrayList底層就是採用elementData這個成員變量進行存儲的。

2.那麼爲什麼ArrayList不會下標越界呢?那ArrayList底層做了什麼呢?

因爲在ArrayList底層封裝了自動擴容的規則.

    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 final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

上面的代碼塊,就是對底層elementData進行擴容,大概邏輯如下:

  • 將數組的長度擴容oldCapacity + (oldCapacity >> 1);擴容1.5倍,因爲是有符號的右移,縮小一倍。>>是位運算符,效率賊高。
  • 如果擴容1.5倍以後,還是不夠用,那麼就把形參的值 (因爲在add()方法中已經把要添加的元素的長度加入了進入,所以該值是擴容後應該的長度) 直接賦值給數組長度
  • 判斷是否大於MAX_ARRAY_SIZE (這是int的最大值-8),如果比這個大,就把int的最大值給他,如果不夠,就拋異常。

繼續看,加油,你看得累,我寫也很累,後面有更精彩的乾貨。

瞭解了擴容以後,記住擴容的方法名字**grow(int minCapacity)**繼續往下看添加的流程

	public boolean add(E e) {
		//size爲添加前數據元素的個數,加一以後,代表加上新添加的元素的長度
		//該方法主要作用是:判斷是否開闢了空間,判斷是否擴容,此處省略,問題2會詳細講述
        ensureCapacityInternal(size + 1); 
        //將elementData的下一份元素賦值爲e
        elementData[size++] = e;
        //返回true
        return true;
    }
    //minCapacity長度:爲ArrayList添加新元素後的長度。
    private void ensureCapacityInternal(int minCapacity) {
    	//這裏暫時略過,後續在java7和java8的時候會講
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
		//帶上minCapacity形參,去下面的方法
        ensureExplicitCapacity(minCapacity);
    }
    
    //minCapacity長度:爲ArrayList添加新元素後的長度。
    private void ensureExplicitCapacity(int minCapacity) {
    	//這是一個快速失敗機制,此處略過
        modCount++;
		//判斷數組長度,如果不夠就擴容,此處省略後續會講
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);  
    }

在添加的方法的最底部,發現每次添加添加操作的時候,都會調用grow(int minCapacity)方法,所以每次向ArrayList添加元素的時候,都會判斷,如果長度不夠,就調用擴容的這個方法進行擴容

問題三:如題:從java7到java8進行了哪些迭代?爲什麼要這樣做?

針對於java7和java8的對比,主要是針對於構造器add方法進行的提升。

可能你累了,但是此時我也類,繼續看下去,你就可以自己手寫一個簡單的ArrayList

1. java8的ArrayList是什麼樣子的?

構造器

	private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

經過查看源碼的無參構造器,創建ArrayList對象時,僅僅給成員變量elementData賦值了一個{},記住這裏:java8的構造器並沒有對成員變量elementData進行開闢默認長度是10的數組

add方法

接下來,詳細解析add方法,你準備好了嗎?

	public boolean add(E e) {
		//size爲添加前數據元素的個數,加一以後,代表加上新添加的元素的長度
		//該方法主要作用是:判斷是否開闢了空間,判斷是否擴容,此處省略,問題2會詳細講述
        ensureCapacityInternal(size + 1); 
        //將elementData的下一份元素賦值爲e
        elementData[size++] = e;
        //返回true
        return true;
    }
    //minCapacity長度:爲ArrayList添加新元素後的長度。
    private void ensureCapacityInternal(int minCapacity) {
    	//這裏暫時略過,後續在java7和java8的時候會講
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
		//帶上minCapacity形參,去下面的方法
        ensureExplicitCapacity(minCapacity);
    }
    
    //minCapacity長度:爲ArrayList添加新元素後的長度。
    private void ensureExplicitCapacity(int minCapacity) {
    	//這是一個快速失敗機制,此處略過
        modCount++;
		//判斷數組長度,如果不夠就擴容,此處省略後續會講
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);  
    }

源碼大意:

  • 因爲默認實例化的時候,並沒有創建對象,所以在ensureCapacityInternal(int minCapacity) 方法 中,進行了判斷,如果第一次的話,就創建對象。

2.java7的ArrayList是什麼樣子的?

構造器

	public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    public ArrayList() {
        this(10);
    }

經過查看源碼的無參構造器,發現無參的構造器調用了有參的構造器,傳入了一個10作爲參數,然後有參的構造器創建一個長度爲10的Object類型的數組,賦值給成員變elementData

add方法


    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }	

源碼大意:

  • 因爲默認實例化的時候,爲成員變量elementData創建了長度爲10的Object數組,所以在ensureCapacityInternal(int minCapacity) 方法 僅僅判斷是否需要擴容,沒有判斷其其否爲{}

3.Java7->8的變化(總結)

JDK7 JDK8
構造器 實例化即開闢數組長度爲10的內存空間 實例化時不開闢內存空間
add()方法 直接添加 第一次調用add方法時,開闢數組長度爲10的內存空間

問題四:如何高效的使用ArrayList?

ArrayList主要實現類,大部分都使用它。但是如果明確知道長度,則建議使用有參構造器,這樣可以降低底層數組的擴容,提高效率。

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

源碼大意 如果你給了一個長度,則會直接創建一個你給的長度的Object類型數組,賦值給成員變量elementData,這樣就減少了擴容的次數,從而提高效率。

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