資料結構與集合之(1)ArrayList 與 Arrays

Ang Ga Ga發表於2020-11-05

    資料結構是指邏輯意義上的資料組織方式及其處理方式。
    從 直接前驅 和 直接後繼 個數的維度來看,大體可以將資料結構分為以下四類:
(1)線性結構
    0 至 1 個直接前驅 和 直接後繼。線性結構包括 順序表、連結串列、棧、佇列等。
(2)樹結構
    0 至 1 個直接前驅 和 0 至 n 個直接後繼(n 大於或等於2 )
(3)圖結構
    0 至 n 個直接前驅 和 直接後繼(n 大於或等於 2)
(4)雜湊結構
    沒有直接前驅和後繼。雜湊結構通過某種特定的雜湊函式將索引 和 儲存的值關聯起來,它是一種查詢效率非常高的資料結構。


一、ArrayList

1、欄位

 private static final long serialVersionUID = 8683452581122892189L;
    //預設容量
    private static final int DEFAULT_CAPACITY = 10;
    //無參構造 或 傳入長度為0 時,是空陣列
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //儲存ArrayList元素的陣列緩衝區。
    transient Object[] elementData; 
    // non-private to simplify nested class access

    //陣列長度
    private int size;

    //最大陣列長度
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

2、構造方法

    先來看 ArrayList 的構造方法:

(1)有參構造:

   &#160注意:如果傳入了初始容量,是直接按照初始容量構建陣列的。·

 public ArrayList(int initialCapacity) {
        //值大於0時,根據構造方法的引數值,忠實地建立一個多大的陣列
        if (initialCapacity > 0) {
        //按傳入初始長度構造陣列
            this.elementData = new Object[initialCapacity];
        } 
        else if (initialCapacity == 0) {
        //如果傳入長度為0 ,為空陣列
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
(2)無參構造

   &#160注意:如果是無參構造,預設的陣列是空陣列,(我老記得是預設容量是10 。QAQ )

  /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
    //預設是空陣列
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

   &#160如果是無參構造,開始建立的是個空陣列,當呼叫 add方法,開始新增元素時,首先會檢查容量是否夠用,這時才把 預設的容量 10 賦給minCapacity ,然後會呼叫 grow 方法擴容,這時 才擴容成長度為 10 的陣列。

3、擴容方法 grow()

    既然是陣列,容量不夠時就需要擴容,ArrayList擴容機制 1.5倍,Vector擴容後機制 2 倍。

private void grow(int minCapacity) {
        // overflow-conscious code
        
        //獲取陣列長度賦給 oldCapacity
        int oldCapacity = elementData.length;

       //新容量=原陣列長度的 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);

//如果新容量小於傳入的引數要求最小容量
        if (newCapacity - minCapacity < 0)
        //這給擴的不夠啊,得按傳入的引數擴
            newCapacity = minCapacity;
        
//如果新容量大於陣列能容納的最大元素個數
if (newCapacity - MAX_ARRAY_SIZE > 0)

//那麼再判斷傳入的引數是否大於MAX_ARRAY_SIZE,
//如果傳入的引數大於MAX_ARRAY_SIZE,那麼新容量等於Integer.MAX_VALUE,
//否則等於MAX_ARRAY_SIZE
            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) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

     說人話:ArrayList 的擴容過程是這樣的,傳入 要求的最小引數 minCapacity ,先獲取當前陣列的長度,以1.5倍擴容,得到新容量 newCapacity,接下來,如果新容量比 要求的最小的還小,那說明擴容得不夠,以要求的最小的為準,即 將要求的最小的容量賦給 新容量。接下來要檢查新容量有沒有超過 允許的陣列的最大容量——int 整型的最大值-8(為什麼要減 8 呢?等會回答。) 如果超過了,可能是因為擴容過頭了,(畢竟是 1.5倍擴容,可能人家傳入的剛好是長度的1.1 呢,本身的oldCapacity不夠,擴了個 1.5 又太多了),那就看看要求的最小的容量是否大於 允許的最大容量,也就是… … -8,如果大於,就把整數最大值 (2^ 31-1)賦給 新容量;如果不大於,說明確實擴容過頭了,把 … .-8賦給新容量。
    之前老看網上說 ArrayList 的最大容量是 2^31-8 ,可是看上面的原始碼,擴容時候,如果要求的最小容量 已經比 …-8 大了,那就應該把 newCapacity 賦為整數的最大值(因為用來衡量陣列的長度是用整數),grow 方法的最後一步是按 呼叫 Arrays 的 copyOf,把原來的陣列元素都拷貝到擴容後的新陣列裡去,所以我覺得 ArrayList 的最大容量是 2 ^ 31,來看 MAX_ARRAY_SIZE 欄位上的註釋:

 /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    有道翻譯:要分配的陣列的最大大小。 有些虛擬機器在一個陣列中保留一些頭 header。試圖分配更大的陣列可能會導致 OutOfMemoryError:請求的陣列大小超過VM限制。所以在不超過 …-8 的前提下,應當是以 … -8 為準的,而超過了,那就只能把 MAX-VALUE 作為陣列的容量啦。
    假如需要將 1000 個元素放置在 ArrayList 中,採用預設構造方法,則需要被動擴容 13 次才可以完成儲存。反之,如果在初始化時便指定了容量 new ArrayLIst(100), 那麼在初始化 ArrayList 物件的時候,就直接分配 1000 個儲存空間,從而避免被動擴容 和 陣列複製的額外開銷。

4、add方法

  • 在末尾新增
 public boolean add(E e) {

//先確保陣列容量是否夠用
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
 private void ensureCapacityInternal(int minCapacity) {
//如果是無參構造建立的空陣列
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

//會按 預設容量10 和 要求最小的容量的值 中 最大值,作為容量最小值,
//所以預設的無參構造是在這一步才
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

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

        // overflow-conscious code

//還是以無參構造為例,這時,minCapacity為10 ,大於 0,需要擴容
        if (minCapacity - elementData.length > 0)

//這時才擴容成大小為 10 的陣列
            grow(minCapacity);
    } 
? ArrayList 的 add 方法執行緒不安全舉例

(1)
elementData[size++] = e; 分兩步執行:elementData [size]=e 和 size++。
假設 size =9,當前的長度是預設的容量 10,先檢查陣列容量是否夠用——無需擴容,接下來,CPU 先給執行緒 A ,執行緒A 將 e1 值賦給 elementData[9] ,執行緒A交出 CPU ,執行緒 B 獲取 CPU ,這時 size 仍為 9 ,執行緒B 會給 elementData[9] 賦值 e2,相當於覆蓋了執行緒 A 的值,然後 CPU 給執行緒 A ,size++ 變為10,CPU再給 執行緒 B ,size++ 變為 11,但是 size [10] 的值卻是 null 的。

(2)
假設 size=9,執行緒A ,先檢查陣列容量是否夠用——無需擴容,CPU 給執行緒B ,——也是 無需擴容。接下來 CPU 給執行緒 A,新增元素 ,size變成10 ,A 執行緒結束,CPU 給 B 執行緒,因為已經檢查過容量,所以 B 執行緒就會直接給 elementDate [10] 進行賦值,這時發生 陣列下標越界異常 ArrayIndexOutOfBoundsException 。

  • 在指定下標處新增
public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!

//將該下標後面的元素都後移動一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

remove

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

//將末尾賦為 null,並將 size-1
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

5、ArrayList 的 iterator 刪除元素

    Collection 就實現了一個介面 Iterable ,這個介面有以下方法:

//iterator 方法很常用的,它會返回 Iterator(是個介面) 的實現類型別
Iterator<T> iterator();
default void forEach(Consumer<? super T> action)
default Spliterator<T> spliterator() 

    所以所有的Collection 介面的實現類肯定都覆寫了 iterator 方法,通過迭代輸出的形式刪除內容:

  package Java;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class TestDemo {
    public static void main(String[] args) {
        List<String> list=new ArrayList<>();
        list.add("Hello");
        list.add("Hello");
        list.add("B");
        list.add("Bit");
        list.add("Bit");
        Iterator<String> iterator=list.iterator();
        while (iterator.hasNext())
        {
            String str=iterator.next();

            if(str.equals("B"))
            {  iterator.remove();
            continue;}

        System.out.println(str);}
    }
}

     注意到,想刪除一個元素,必須先跳過該元素,next 和 remove 方法是互相依賴的。
    來看看 next 和 remove 的原始碼:

//ArrayList 有個內部類 Itr 實現了 Iterator 介面
private class Itr implements Iterator<E> {


        int cursor;       // index of next element to return

//上一個元素的下標
        int lastRet = -1; // index of last element returned; -1 if no such
        
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

  public E next() {
//快速失敗機制
            checkForComodification();
//指標,需要返回的元素的下標
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();


            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
 public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
            
//this:在內部類的某個方法中 指定某個巢狀層次的 外圍類的 "this"
//呼叫 ArrayList 的 remove 方法,即把下標以後的元素依次向前移一位,末尾賦 null
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            }
             catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

    如果先呼叫 remove ,指向前一個元素的指標 lastRet 是指向 -1 的,陣列下標越界,所以必須結合 next() ,先讓 cursor、lastRet 都往後挪一個,然後呼叫 remove 才能刪除 lastRet 也就是這時候的[0],刪除元素後,再把 cursor、lastRet 挪回原位——0,-1。
在這裡插入圖片描述

ArrayList 和 LinkedList 區別

(1).是否保證執行緒安全:
     ArrayList 和 LinkedList 都是不同步的,也就是不保證執行緒安全;
(2)底層資料結構:
    ArrayList 的底層是 Object陣列,可以實現動態擴容,每次擴容成原來的 1.5 倍,支援隨機訪問,但是刪除或者插入元素效率很低,例如在陣列中間插入(刪除)一個元素,必須把這個元素以後的元素全部向後(前)搬移一個位置。
    LinkedList 底層使用的是雙向連結串列資料結構(JDK1.6之前為迴圈連結串列,JDK1.7取消了迴圈。不存在初始容量和擴容的概念。訪問一個元素要遍歷節點,時間複雜度為 O(n)。刪除或者插入一個給定指標指向的結點時間複雜度為 O(1)。但是 LinkedList 的remove(Object o) 方法時間複雜度是 O(n^2) 的,因為要先遍歷找到刪除的節點。
(3)記憶體空間佔用:
    ArrayList的空 間浪費主要體現在在 list 列表的結尾會預留一定的容量空間【8】,而LinkedList的空間花費則體現在它的每一個元素都需要消耗比 ArrayList更多的空間(因為要存放直接後繼和直接前驅以及資料)

二、Arrays

    Arrays 是針對陣列物件進行操作的工具類,包括陣列的排序、查詢、對比、拷貝等操作。(尤其是排序,在多個 JDK 版本中不斷地進化,比如原來的歸併排序改成 Timsort,明顯地改善了集合的排序效能。)
    陣列 與 集合 都是用來儲存物件的容器,前者性質單一,方便易用;後者型別安全,功能強大,且兩者之間必然有互相轉換的方式。 在陣列轉集合的過程中,注意是否使用了檢視方式直接返回陣列中的資料。比如 Arrays.asList() 為例,它把陣列轉換成集合時,不能使用其 修改集合相關的方法,它的 add / remove / clear 方法會丟擲 UnsupportedOperationException 異常。
?

import java.util.Arrays;
import java.util.List;

public class ArrayAsList {
    public static void main(String[] args) {
        String[] stringArray=new String[3];
        stringArray[0]="one";
        stringArray[1]="two";
        stringArray[2]="three";

        List<String> stringList= Arrays.asList(stringArray);
        //修改轉換後的集合,將第一個元素“one”改成“oneLIst”
        stringList.set(0,"oneList");
        System.out.println(stringArray[0]);

        stringList.add("four");
        stringList.remove(2);
        stringList.clear();
    }
}

執行結果:
在這裡插入圖片描述
     可以通過 set() 方法修改元素的值,原有陣列相應位置的值同時也會被修改,但是不能進行修改元素個數的任何操作,否則 編譯正確,但是均會丟擲 UnsupportedOperationException 異常。 Arrays.asList 體現的是介面卡模式,後臺的資料仍是原有陣列,set() 方法即間接對陣列進行值的修改操作。Arrays.asList 返回物件是一個 Arrays 的內部類,它並沒有實現集合個數的相關修改方法,這也是丟擲異常的原因之一。原始碼如下:

    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

     返回的明明是 ArrayList 物件,咋就不能隨心所欲對集合進行修改呢? 注意 此 ArrayList 非彼 ArrayList,雖然 Arrays 和 ArrayList 同屬一個包,但是在 Arrays 類中還定義了一個 ArrayList 的內部類,根據作用域就近原則,此處的 ArrayList 是李鬼?,即 這是個內部類。此 李鬼十分簡單,只提供了個別方法的實現:

在這裡插入圖片描述

     會丟擲 UnsupportedOperationException 異常 ,是因為它的父類 AbstractList:

public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
 public E remove(int index) {
        throw new UnsupportedOperationException();
    }

     好傢伙,這貨有氣節,它傳遞的資訊是 “要麼直接用我,要麼小心異常!”。陣列轉集合引發故障還是很常見的,比如,某業務呼叫某介面時,對方以這樣的方式返回一個 List 型別的集合物件,本方獲取集合資料時,99.9% 是隻讀操作,但小概率需要增加一個元素,就會引發故障。所以, 在使用陣列轉集合時,需要使用 李逵 java.util.ArrayList 直接建立一個新集合,引數就是 Arrays.asList 返回的不可變集合 ,原始碼如下:

List<Object> objectList=new java.util.ArrayList<Object>(Arrays.asList(陣列));

    相比於 陣列 轉 集合,集合 轉 陣列 更加可控,畢竟是從相對自由的集合容器 轉為 更加苛刻的 陣列。比如,適配別人的陣列介面,或者 進行區域性方法計算等,都可能遇到 集合 轉 陣列的情況。

? 例:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class LIstToArray {
    public static void main(String[] args) {
        List<String> list=new ArrayList<String>(3);
        list.add("one");
        list.add("two");
        list.add("three");

        //(1)
        Object[] array1=list.toArray();

        //(2)
        String[] array2=new String[2];
        list.toArray(array2);
        System.out.println(Arrays.asList(array2));

        //(3)
        String[] array3=new String[3];
        list.toArray(array3);
        System.out.println(Arrays.asList(array3)); 
    }
}

執行結果:
在這裡插入圖片描述
     執行成功了,從 (1) 可以看出,list.toArray() 返回的是 Object[ ] ,改程式碼後發現,不能用 String[ ] 去接,儘管返回陣列的每個元素都是 String 型別的,但就是不能用 String[ ] 去接,書上說這樣說明泛型丟失不要用 toArray() 無參方法把集合轉換成陣列。來看看 toArray 方法的原始碼,就懂了:

public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

關鍵在這,返回的是 Object[ ] 型別的陣列 elementData:

  transient Object[] elementData; // non-private to simplify nested class access

(同時也注意到,這個儲存 ArrayList 真實資料的陣列由 transient 修飾,表示此欄位在類的序列化時常被忽略。因為集合序列化時 系統會呼叫 writeObject 寫入流中,在網路客戶端反序列化的 readObject 時,會重新賦值到 新物件的 elementData 中。點解多此一舉? 因為 elementData 容量經常會大於 實際儲存元素的數量,所以只需傳送真正有價值的陣列元素即可。)

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

但是吧,複製值的時候,傳進來的是泛型 U[ ] 陣列,所以 拷貝的原料 original 裡的元素是啥型別,最後結果裡的陣列的元素也就是啥型別(個人理解,如有異議請及時指出):

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        //新建一個陣列 copy
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

     從 (2)可以看出,傳入的陣列的容量不夠,輸出值是 null,來看原始碼:

@SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);

        if (a.length > size)
            a[size] = null;
            
        return a;
    }

     最後來用程式碼模擬一下 入引數組容量不夠時、入引數組容量剛好時,以及 入引數組容量超過集合大小時,大概的執行時間,程式碼如下:

import java.util.ArrayList;
import java.util.List;

public class ToArraySpeedTest {
    private static final int COUNT=100 * 100 * 100;
    public static void main(String[] args) {
        List<Double> list=new ArrayList<>(COUNT);

        //構造一個 100 萬個元素的測試集合
        for(int i=0;i<COUNT;i++)
        {list.add(i * 1.0);}

        long start=System.nanoTime();

        Double[] notEnoughArray=new Double[COUNT-1];
        list.toArray(notEnoughArray);

        long middle1=System.nanoTime();

        Double[] equalArray=new Double[COUNT];
        list.toArray(equalArray);

        long middle2=System.nanoTime();

        Double[] doubleArray=new Double[COUNT * 2];
        list.toArray(doubleArray);
        long end=System.nanoTime();

        long notEnoughArrayTime=middle1-start;
        long equalEnoughArrayTime=middle2-middle1;
        long doubleArrayTime=end-middle2;
        System.out.println("陣列容量小於集合大小:notEnoughArrayTime:"+notEnoughArrayTime/
                (1000.0 * 1000.0) + "ms");
        System.out.println("陣列容量等於集合大小:equalEnoughArrayTime:"+equalEnoughArrayTime/
                (1000.0 * 1000.0) + "ms");

        System.out.println("陣列容量大於集合大小:doubleArrayTime:"+doubleArrayTime/
                (1000.0 * 1000.0) + "ms");
    }
}

執行結果:
陣列容量小於集合大小:notEnoughArrayTime:85.5096ms
陣列容量等於集合大小:equalEnoughArrayTime:7.1272ms
陣列容量大於集合大小:doubleArrayTime:8.7597ms
    
    具體的執行時間,由於 CPU 資源佔用的隨機性,會有一定差異,多次執行結果顯示,當陣列容量等於集合大小時,執行總是最快的,空間消耗也是最少的。由此證明,如果陣列初始大小設定不當,不僅會降低效能,還會浪費空間。使用集合 toArray(T[ ] array) 方法,轉換成陣列時,注意需要傳入型別完全一樣的陣列,並且它的容量大小為 list.size( ) 。

相關文章