全面解析ArrayList,超詳細!

寫在前面:java

小夥伴兒們,你們好!今天來學習ArrayList相關內容,做爲面試必問的知識點,來深刻了解一波!程序員

思惟導圖:面試

ArrayList學習圖

1,ArrayList底層數據結構

ArrayList就是動態數組,是List接口的可調整大小的數組實現;除了實現List接口以外,該類還提供了一些方法來操縱內部使用的存儲列表的數組大小。它的主要底層實現是數組Object[] elementData。算法

數組的特色你們都知道,遍歷查詢速度快——數組在內存是連續空間,能夠根據地址+索引的方式快速獲取對應位置上的元素。可是它的增刪速度慢——每次刪除元素,都須要更改數組長度、拷貝以及移動元素位置。數組

ArrayList類架構圖

ArrayList 是 java 集合框架中比較經常使用的數據結構了。繼承自 AbstractList,實現了 List 接口。底層基於數組實現容量大小動態變化。容許 null 的存在。同時還實現了 RandomAccess、Cloneable、Serializable 接口,因此ArrayList 是支持快速訪問、複製、序列化的。緩存

與ArrayList相似的是LinkedList,可是LinkedList底層是鏈表,它的數組遍歷速度慢,但增刪速度很快。安全

小結:數據結構

ArrayList底層是數組實現的存儲,查詢效率高,增刪效率低。多線程

2,ArrayList構造方法

下面是查看API中構造方法架構

構造方法

2.1,無參構造方法

咱們看源碼中的無參構造方法:

無參構造,使用默認的size爲10的空數組,在構造方法中沒有對數組長度進行設置,會在後續調用add方法的時候進行擴容。

無參構造

裏面是一個賦值操做,右邊是一個空容量數組,左邊則是存儲數據的容器,如下是參照源碼分析;

//默認空容量數組,長度爲0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//集合真正存儲數據的容器
transient Object[] elementData; 

2.2,參數爲指定初始化容量的構造方法

來看看源碼中的int型構造方法:

指定初始化容量構造

參數大於0,elementData初始化爲initialCapacity大小的數組;
參數等於0,elementData初始化爲空數組;
參數小於0,拋出異常;

2.3,參數爲Collection類型的構造方法

來看看構造方法的源碼:

Collection類型構造

將一個參數爲Collection的集合轉變爲ArrayList(實際上就是將集合中的元素換爲了數組的形式);若是傳入的集合爲null會拋出空指針異常。
c.toArray()可能不會正確地返回一個 Object[]數組,那麼使用Arrays.copyof()方法。
若是集合轉換成數組以後數組長度爲0,那就直接使用本身的空成員變量初始化elementData。

總結:

上面的構造方法理解起來比較簡單,無參構造和初始化容量構造的目的都是初始化底層數組elementData(this.elementData=XXX);

無參構造方法會將elementData初始化一個空數組,插入元素時,擴容將會按默認值從新初始化數組;
有參構造方法會將elementData初始化爲參數值大小(>=0)的數組;

若是在構造 ArrayList 實例時,指定初始化值(初始化容量或者集合),那麼就會建立指定大小的 Object 數組,並把該數組對象的引用賦值給 elementData;若是不指定初始化值,在第一次添加元素值時會使用默認的容量大小 10 做爲 elementData 數組的初始容量,使用 Arrays.conpyOf() 方法建立一個 Object[10] 數組。

通常狀況下,咱們用默認的構造方法便可。上面說到使用無參構造時會調用add方法並進行擴容,下面來看看add方法以及擴容的細節。

3,添加add()方法分析

看看ArrayList的add()添加方法:

add()添加方法

3.1,列表的末尾添加指定元素

public boolean add(E e)

先來看看源碼分析

add()源碼

咱們先來看第一個添加方法add(E e),具體流程以下:

//將添加的數據傳入給e 
public boolean add(E e) { 
    //調用方法對內部容量進行校驗 
    ensureCapacityInternal(size + 1); 
    elementData[size++] = e; 
    return true; 
} 

咱們看到add方法中在添加元素以前,會先判斷size的大小,因此咱們來看看ensureCapacityInternal方法的細節之處

private void ensureCapacityInternal(int minCapacity) { 
        //判斷集合存數據的數組是否等於空容量的數組 
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 
        //經過最小容量和默認容量 出較大值 (用於第一次擴容) 
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); 
        } 
        //將if中計算出來的容量傳遞給下一個方法,繼續校驗 
        ensureExplicitCapacity(minCapacity); 
    } 

當 要 add 進第1個元素時,minCapacity爲(size+1=0+1=)1,在Math.max()方法比較後,minCapacity 爲10。而後緊接着調用ensureExplicitCapacity更新modCount的值,並判斷是否須要擴容。接下來看ensureExplicitCapacity方法

private void ensureExplicitCapacity(int minCapacity) { 
        //實際修改集合次數++ (在擴容的過程當中沒用,主要是用於迭代器中) 
        modCount++; 
        //判斷最小容量    - 數組長度是否大於    0 
        if (minCapacity - elementData.length > 0) 
        //將第一次計算出來的容量傳遞給    核心擴容方法 
        grow(minCapacity); 
    }

而後是擴容的核心**grow()**方法:

private void grow(int minCapacity) { 
        //記錄數組的實際長度,此時因爲木有存儲元素,長度爲0 
        int oldCapacity = elementData.length; 
        //核心擴容算法   原容量的1.5倍 
        int newCapacity = oldCapacity + (oldCapacity >> 1); 
        //判斷新容量-最小容量是否小於0, 若是是第一次調用add方法必然小於
        if (newCapacity - minCapacity < 0) 
        //仍是將最小容量賦值給新容量 
        newCapacity = minCapacity; 
        //判斷新容量-最大數組大小是否>0,若是條件知足就計算出一個超大容量
        if (newCapacity - MAX_ARRAY_SIZE > 0) 
        newCapacity = hugeCapacity(minCapacity); 
        // 調用數組工具類方法,建立一個新數組,將新數組的地址賦值給
        elementData elementData = Arrays.copyOf(elementData, newCapacity); 
}

執行流程:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

3.2,指定位置添加指定元素

public void add(int index, E element)

先來看看源碼分析

//在元素序列index位置處插入
public void add(int index, E element) {
    rangeCheckForAdd(index);
    //1,檢測是否須要擴容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //2,將index及其以後的元素都向後移一位
    System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
    // 3. 將新元素插入至 index 處
    elementData[index] = element;
    size++;
}
private void rangeCheckForAdd(int index) {
    //這裏判斷的index>size,index小於0,超出指定範圍就報錯
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

咱們再看看它的使用方法:

import java.util.ArrayList;

/**
 * @author 公衆號:程序員的時光
 * @create 2020-11-03 17:05
 * @description
 */
public class Test03 {
    public static void main(String[] args) {
        ArrayList<String> list=new ArrayList<>();
        list.add("海心焰");
        //在index爲1的位置處添加數據
        list.add(1,"玄黃炎");
        System.out.println(list);
    }
}

運行結果:

結果

3.3,按照指定的元素順序,將全部元素添加到此列表的尾部

public boolean addAll(Collection<? extends E> c);

這個方法的描述是,按指定集合的Iterator返回的順序將指定集合中的全部元素追加到此列表的末尾。

簡單來說,就是將一個集合的元素所有添加到另一個集合中去。

代碼

運行結果:

結果

3.4,將指定集合中的全部元素插入到此列表中,從指定位置開始

public boolean addAll(int index, Collection<? extends E> c);

這個方法和上面的方法都是把一個集合中的元素添加到另一個集合中去,不一樣的在於上面的方法是默認添加至目標集合的尾部,而此方法是包含索引的,也就是在指定位置開始插入元素。

代碼

運行結果:

結果

4,其餘方法分析

ArrayList包括不少方法,咱們來簡單回顧一下。

//移除指定位置上的元素
public E remove(int index);
//移除此列表中首次出現的指定元素(若是存在)
boolean remove(Object o);
//修改集合元素
public E set(int index, Object o);
//查找集合元素
public E get(int index);
//清空集合全部數據
public void clear();  
//判斷集合是否包含指定元素
public boolean contains(Object o);
//判斷集合是否爲空
public boolean isEmpty()

5,常見面試題(精華)

5.1,ArrayList是如何擴容的?

這個請參照3.1章節的擴容步驟,來看看它的核心擴容方法:

image-20201106074800546

總結:

  1. 擴容的大小是原先數組的1.5倍;
  2. 若值newCapacity比傳入值minCapacity還要小,則使用傳入minCapacity,若newCapacity比設定的最大容量大,則使用最大整數值;

5.2,ArrayList頻繁擴容致使性能降低,如何處理?

比方說如今須要往數組裏添加10w條數據,咱們來看看先後的時間變化:

使用指定初始化容量的構造方法

結果是:

結果

ArrayList底層是數組實現的,那麼每次添加數據時會不斷地擴容,這樣的話會佔內存,性能低,因此致使時間很長。

咱們能夠用ArrayList的指定初始化容量的構造方法來解決性能低的問題。

5.3,ArrayList在增刪元素的時候是怎麼進行的,還有爲什麼很慢?

在前面咱們說過,它有按照索引添加,也有直接添加的。在這以前須要校驗長度問題ensureCapacityInternal,若是長度不夠,則須要進行擴容操做。

而前面的擴容是擴大原來的1.5倍,採用位運算,右移一位。

image-20201106091109324

若是後面的數據量級過大,在100萬條數據中新增一個元素,後面的元素都要拷貝以及移動位置,因此說效率很低。

5.4,ArrayList是線程安全的嗎?

ArrayList線程是不安全的。線程安全的數組容器是Vector,它的原理是把全部的方法都加上synchronized。

咱們來測試一下,先準備一個線程任務類:

image-20201106151148502

而後定義測試類,對任務類進行測試:

image-20201106151319139

咱們來看結果:

image-20201106151419938

能夠看到會報異常錯誤,有的線程仍是爲null,這說明ArrayList線程是不安全的。

固然能夠用線程安全的集合Vector來代替ArrayList

Vector集合

或者咱們能夠直接加synchronized關鍵字,把不安全的線程變成安全的:

加關鍵字synchronized

這樣也是能夠保證線程安全的。

爲啥ArrayList線程不安全?

線程不安全:

線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致或者數據污染。

線程不安全就是不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據是髒數據。

List接口下面有兩個實現,一個是ArrayList,另一個是Vector。從源碼的角度分析,由於Vector的方法前加了synchronized關鍵字。

ArrayList在添加一個元素時,有兩步來完成,1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。

在單線程運行的狀況下,若是 Size = 0,添加一個元素後,此元素在位置 0,並且 Size=1;

而若是是在 多線程狀況下,好比有兩個線程,線程 A 先將元素存放在位置 0。可是此時 CPU 調度線程A暫停,線程 B 獲得運行的機會。線程B也向此 ArrayList 添加元素,由於此時 Size 仍然等於 0(注意哦,咱們假設的是添加一個元素是要兩個步驟哦,而線程A僅僅完成了步驟1),因此線程B也將元素存放在位置0。而後線程A和線程B都繼續運行,都增長 Size 的值。

那好,咱們來看看 ArrayList 的狀況,元素實際上只有一個,存放在位置 0,而 Size 卻等於 2。這就是「線程不安全」了。

5.5,ArrayList和LinkedList區別

  1. 底層數據結構:ArrayList底層使用的是數組;LinkedList底層使用的是雙向鏈表;
  2. 插入和刪除元素操做:ArrayList採用的是數組存儲,因此插入和刪除元素是跟元素的位置有關係。LinkedList採用的是鏈表存儲,刪除元素是不受元素位置影響的;若是是要在指定位置i插入和刪除的話((add(int index,E element))時間複雜度近似爲O(n),由於須要先移動再插入。
  3. 隨機訪問:ArrayList對於隨機元素訪問的效率明顯比LinkedList高。隨機訪問就是經過元素的索引來獲取元素(也就是set和get(int index)方法)。
  4. 線程不安全:ArrayList和LinkedList都是不一樣步的,也就是說都是線程不安全的。
  5. 接口實現:ArrayList實現了RandomAccess能夠支持隨機元素訪問,而LinkedList實現了Deque能夠當作隊列使用
  6. 內存空間佔用狀況:ArrayList的空間佔用主要體如今list列表的末尾會有必定的容量空間,它的優點在於內存的連續性,CPU的內部緩存結構會緩存連續的內存片斷,能夠大幅度下降內存的性能開銷,提升效率;LinkedList的空間佔用體如今每個元素都須要消耗空間內存,要存放前驅後繼等數據。

當操做是在一列數據的後面添加數據而不是在前面或中間,而且須要隨機地訪問其中的元素時,使用ArrayList會提供比較好的性能;
當操做是在一列數據的前面或中間添加或刪除數據,而且按照順序訪問其中的元素時,使用LinkedList會更好。


相關文章
相關標籤/搜索