3秒搞定ArrayList

老兵程式設計師發表於2021-06-15

Jdk8

總結

image

  1. 擴容和刪除
 ArrayList 就是一個實現了List介面的可自動擴容的陣列,當新增元素的時候它會嘗試擴容,擴容的標準是變為原來的1.5倍**,當刪除元素的時候,它會左移元素,避免陣列出現"空位"
  1. 容量
 new ArrayList<>() 初始化容量為0,等到第一次add的時候再初始化為10
  1. 有序集合
  2. 可以儲存重複值和null值

    示例:

    public static void main(String[] args) {
        List<String> a=new ArrayList<>();
        a.add(null);
        a.add(null);
        a.add(null);
        System.out.println(a.size());
    }
    輸出:
    3
  1. ArrayList 是快速失敗的,在遍歷的同時當集合被修改後會丟擲ConcurrentModificationException,可以使用Iterator 的刪除方法來避免這個問題
  2. 非執行緒安全的,如果你想在多執行緒環境中使用,可以使用Vector 或者它的執行緒安全包裝類
  3. 擴充套件
  作業系統的區域性性原理,陣列的連續儲存空間的特性充分使用了區域性性原理,也就是說硬體的快取記憶體加速了陣列的訪問

效能

  1. Adding an element- 如果你使用的是  add(E e) 方法新增一個元素到ArrayList末尾 ,它的時間複雜度 O(1);但是當空間不足引發擴容的時候,會導致新建陣列然後複製資料,這個時候它的時間複雜度 O(n) ;當你使用 add(int index, E element)的時候它的演算法複雜度是 O(n - index) 也就是 O(n)
  2. Retrieving an element- 當你使用get(int index) 的時候,它的時間複雜度是 O(1),因為陣列可以直接根據下標進行定位
  3. Removing an element- 當你使用 remove(int index) 它的時間複雜度是 O(n - index) ,因為它涉及到移動元素
  4. Traverse - 遍歷的時間時間複雜度是O(n),也就是依賴於Capacity 的大小,如果你比較重視遍歷的效能,就請不要不要給它設定一個很大的初始容量

UML

image
底層是一個Object[],新增到ArrayList中的資料儲存在了elementData屬性中。

  1. 當呼叫new ArrayList<>()時,將一個空陣列 DEFAULTCAPACITY_EMPTY_ELEMENTDATA  賦值給了elementData,這個時候集合的長度size為預設長度0
  1. 例如當呼叫new ArrayList<>(100)時,根據傳入的長度,new一個Object[100]賦值給elementData,當然如果玩兒的話,傳了一個0,那麼將一個空陣列 EMPTY_ELEMENTDATA 賦值給了elementData
  1. 例如當呼叫new ArrayList<>(new HashSet())時,根據原始碼,我們可知,可以傳遞任何實現了Collection介面的類,將傳遞的集合呼叫toArray()方法轉為陣列內賦值給elementData

構造方法

無參構造

建立一個空的使用預設容量的list(預設是0,第一次add會初始化為10)

//預設建立一個ArrayList集合
List<String> list = new ArrayList<>();
/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

指定初始容量

建立一個空的指定容量的list

//建立一個初始化長度為100的ArrayList集合
List<String> initlist = 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);
    }
}

其他集合作為引數

//將其他型別的集合轉為ArrayList
List<String> setList = new ArrayList<>(new HashSet());

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

構造一個包含指定集合元素的列表,其順序由集合的迭代器返回。當傳入的集合引數為空的話,丟擲NullPointerException,因為它會呼叫該集合的toArray 方法,和HashTable 裡面呼叫key 的hashcode 方法的原理一樣

當集合是一個空的集合的話,elementData = EMPTY_ELEMENTDATA和指定0是initialCapacity的效果一樣


注意在傳入集合的ArrayList的構造方法中,有這樣一個判斷

if (elementData.getClass() != Object[].class),
給出的註釋是:c.toArray might (incorrectly) not return Object[] (see 6260652),即呼叫toArray方法返回的不一定是Object[]型別,檢視Collection介面的定義
Object[] toArray();
我們發現返回的確實是Object[],那麼為什麼還會有這樣的判斷呢?
如果有一個類CustomList繼承了ArrayList,然後重寫了toArray()方法呢。。
public class CustomList<E> extends ArrayList {
    @Override
    public Integer [] toArray() {
        return new Integer[]{1,2};
    };
    
    public static void main(String[] args) {
        Object[] elementData = new CustomList<Integer>().toArray();
        System.out.println(elementData.getClass());
        System.out.println(Object[].class);
        System.out.println(elementData.getClass() == Object[].class);
    }
}

執行結果:

class [Ljava.lang.Integer;
class [Ljava.lang.Object;
false


接著說,如果傳入的集合型別和我們定義用來儲存新增到集合中值的Object[]型別不一致時,ArrayList做了什麼處理?讀原始碼看到,呼叫了
Arrays.copyOf(elementData, size, Object[].class);

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {    
    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;
}

我們發現定義了一個新的陣列,將原陣列的資料複製到了新的陣列中去。

思考

    我們在檢視 ArrayList 的實現類原始碼時,你會發現物件陣列 elementData 使用了 transient 修飾,我們知道 transient 關鍵字修飾該屬性,則表示該屬性不會被序列化,然而我們並沒有看到文件中說明 ArrayList 不能被序列化,這是為什麼?<br />

ArrayList 屬性主要由陣列長度 size、物件陣列 elementData、初始化容量 default_capacity 等組成, 其中初始化容量預設大小為 10

// 預設初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 物件陣列
transient Object[] elementData; 
// 陣列長度
private int size;

從 ArrayList 屬性來看,它沒有被任何的多執行緒關鍵字修飾,但 elementData 被關鍵字 transient 修飾了。這就是我在上面提到的第一道測試題:transient 關鍵字修飾該欄位則表示該屬性不會被序列化,但 ArrayList 其實是實現了序列化介面,這到底是怎麼回事呢?

這還得從"ArrayList是基於陣列實現"開始說起,由於 ArrayList 的陣列是基於動態擴增的,所以並不是所有被分配的記憶體空間都儲存了資料。
如果採用外部序列化法實現陣列的序列化,會序列化整個陣列。ArrayList 為了避免這些沒有儲存資料的記憶體空間被序列化,內部提供了兩個私有方法 writeObject 以及 readObject 來自我完成序列化與反序列化,從而在序列化與反序列化陣列時節省了空間和時間。因此使用 transient 修飾陣列,是防止物件陣列被其他外部方法序列化


                        看到這裡就點個贊吧?分享更多技術文章去幫助更多的人,這裡有我所有知識庫喲~ ?

                                                     https://www.yuque.com/yingwen...

相關文章