《咱們一塊兒學集合》-ArrayList

微信關注【面試情報局】咱們一塊兒幹翻面試官。

1.前言

今天咱們要研究的集合是ArrayList,在咱們學習ArrayList以前,咱們先看看面試官是如何利用ArrayList的相關知識點來吊打咱們得。java

  1. ArrayList的底層結構是什麼?
  2. ArrayList的初始化容量是多少?
  3. ArrayList的容量會變嗎?是怎麼變化滴?
  4. ArrayList是線程安全的嗎?
  5. ArrayList和LinkedList有什麼區別?

看了這些面試題,是否是心裏以爲:面試

image

言歸正傳,下面咱們就經過ArrayList源碼學習來解決解決上述問題。segmentfault

2.概述

ArrayList是基於數組,支持自動擴容的一種數據結構。相比數組來講,由於他支持自動擴容,而且內部實現了不少操做數組的方法,因此成爲咱們平常開發中最經常使用的集合類。其內部結構以下:數組

image

3.類圖

image

  • AbstractList 抽象類,提供了List接口的相關實現和迭代邏輯的實現,不過對ArrayList意義不大,由於ArrayList大量重寫了AbstractList的實現
  • List 接口,定義了數組的增刪改查迭代遍歷等相關操做。
  • Cloneable 接口,支持ArrayList克隆
  • Serializabel 接口,支持ArrayList序列化與反序列化
  • RandomAccess 接口,支持ArrayList快速訪問

4.屬性

先讓咱們看看ArrayList的源碼:安全

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 默認初始容量。
    private static final int DEFAULT_CAPACITY = 10;

    // 用於空實例的共享空數組(建立空實例時使用)
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 用於默認大小的空實例的共享空數組實例。
    // 咱們將其與EMPTY_ELEMENTDATA區分開來,以便知道添加第一個元素時要膨脹多少。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 存儲數組列表元素的數組緩衝區。arrayList的容量就是這個數組緩衝區的長度。
    // 任何空的ArrayList 將被擴展到10當(第一次添加元素時)
    // 注意是經過transient修飾
    transient Object[] elementData; // non-private to simplify nested class access

    // 數組列表的大小(它包含的元素數量)
    private int size;

    /* 要分配的數組的最大大小
     * 嘗試分配更大的數組可能會致使OutOfMemoryError:請求的數組大小超過VM限制*/
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    // 該屬性是經過繼承 AbstractList 得來,列表修改的次數(版本號)
    protected transient int modCount = 0;
}

經過源碼咱們能夠知道到:微信

  • DEFAULT_CAPACITY 表示ArrayList的初始容量(採用無參構造時第一次添加元素擴容的容量,後面會介紹),默認是10
  • elementData 表示ArrayList實際儲存數據的數組,是一個Object[]
  • size 表示該ArrayList的大小(就是elementData包含的元素個數)。
  • MAX_ARRAY_SIZE 表示ArrayList能分配的最大容量 Integer.MAX_VALUE - 8
  • modCount 表示該ArrayList修改的次數,在迭代時能夠判斷ArrayList是否被修改。

看到這裏,咱們就能夠很輕鬆回答上面的1和2兩個問題。數據結構

  1. ArrayList的底層結構是什麼?
  2. ArrayList的初始化容量是多少?

ArrayList底層實現就是一個數組,其初始容量是10app

image

5.經常使用方法

5-1.構造函數

首先仍是讓咱們看看源碼,由於源碼最有說服力。

dom

// 使用指定的初始容量構造一個空列表。
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA; // 若是爲0使用默認空數組
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    }
}

/*Constructs an empty list with an initial capacity of ten.
* 構造一個初始容量爲10的空列表。(在第一次擴容時容量才爲10,如今仍是null)*/
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 構造一個包含指定集合的元素的列表,按照集合的迭代器返回它們的順序。
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray(); // 將集合轉變爲數組
    // 賦值 size 並判非 0
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652) 這是一個bug在java9已經被解決
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

經過查看源碼咱們能夠發現:jvm

  • ArrayList 有三個構造函數:指定初始化大小構造,無參構造,指定初始化數據構造
  • ArrayList的無參構造,其實默認是空數組,咱們上面說的初始化容量默認爲10,是當咱們用無參構造函數後,第一次向ArrayList添加元素時擴容的默認大小。

5-2.增長

ArrayList添加元素的方法有四個:一個是在末尾添加,一個是指定索引添加,另兩個是在末尾添加集合和在指導索引位置添加集合

// 將指定的元素添加到列表的末尾。
public boolean add(E e) {
    // 確保容量足夠
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

// 在列表指定的位置插入指定的元素。
// 將當前位於該位置的元素(若是有的話)和隨後的元素向右移動(下標加1)。
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++; // 大小加 1
}

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 
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 增長容量,以確保至少能夠保存由最小容量(minCapacity)參數指定的元素數量。
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 1.5倍擴容
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0) // 擴容後不知足指望大小則以指望大小做爲容量
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) // 分配jvm的最大容量,防溢出
        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;
}

// 將指定集合中的全部元素追加到此列表的末尾。按照指定集合的迭代器返回它們的順序。
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray(); // 集合轉數組
    int numNew = a.length;    // 獲取要添加的長度
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew); // 經過元素拷貝來追加元素
    size += numNew;
    return numNew != 0;
}

// 將指定集合中的全部元素插入到此列表中,從指定位置開始。
// 新元素將按照指定集合的迭代器返回的順序出如今列表中。
public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index); // 檢查索引是否合法

    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount

    int numMoved = size - index;
    if (numMoved > 0) // 騰出空位
        System.arraycopy(elementData, index, elementData, index + numNew,numMoved);
    // 將a拷貝到elementData
    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

image

經過源碼咱們知道ArrayList添加元素大體流程以下:

image

經過源碼咱們須要注意:

  • 擴容是原容量+原容量大小一半,也就是說是按照1.5倍擴容:oldCapacity + (oldCapacity >> 1),但最後的容量並不必定是按照這個規則計算獲得的大小,由於他還有兩個if判斷。
  • ArrayList中數組最大隻能分配Integer.MAX_VALUE,在大就會致使OutOfMemoryError
  • ArrayList擴容時有許多溢出判斷操做,這很是值得借鑑。
  • ArrayList擴容底層調用的是System.arraycopy(Object src,int srcPos,Object dest, int destPos,int length)方法,每一個參數對應爲(原始數組,起始位置,目標數組,起始位置,拷貝大小)

看到這裏咱們能夠回答第3個問題:

ArrayList的容量會變嗎?是怎麼變化滴?

數組容量會改變,改變的規則是按照原數組1.5倍進行擴容,但最終容量不必定是經過該規則計算獲得的值,由於後面有兩個if判斷:1.是否知足指望容量;2.是否超出jvm分配的最大容量

image

5-3.刪除

ArrayList刪除元素的方法有四個:刪除指定索引位置的元素,刪除指定元素,刪除指定集合元素和經過過濾器刪除

// 刪除列表中指定位置的元素。將全部後續元素向左移動(從它們的下標減去1)。
public E remove(int index) {
    // 確保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;
}

// 從列表中刪除指定元素的第一個匹配項,若是它存在的話並返回 true。
public boolean remove(Object o) {
    if (o == null) { // 空值單獨刪除,由於add時也沒有對null進行效驗
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index); // 移除元素
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) { // 經過equals比較,若是是自定義對象元素,必定要重寫它
                fastRemove(index);
                return true;
            }
    }
    return false;
}

// 跳過邊界檢查的移除方法(由於已經被驗證邊界合法)
private void fastRemove(int index) {
    modCount++;
    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
}

// 今後列表中刪除指定集合中包含的全部元素。
// 若是此列表包含空元素,而指定的集合不容許空元素則會拋出NullPointerException
public boolean removeAll(Collection<?> c) {
    // 判斷是否爲null
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}

// 經過不一樣complement來操做列表
private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++) // complement決定操做行爲
            if (c.contains(elementData[r]) == complement) 
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        if (r != size) {
            System.arraycopy(elementData, r,elementData, w,size - r);
            w += size - r; 
        }
        if (w != size) { // 將刪除的元素賦null
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

@Override
public boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    // figure out which elements are to be removed 找出要刪除的元素
    // any exception thrown from the filter predicate at this stage
    // will leave the collection unmodified
    int removeCount = 0;
    final BitSet removeSet = new BitSet(size); // 記錄要刪除元素的集合
    final int expectedModCount = modCount; // 記錄版本號
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        @SuppressWarnings("unchecked")
        final E element = (E) elementData[i];
        if (filter.test(element)) { // 記錄要刪除的元素index
            removeSet.set(i);
            removeCount++;
        }
    }
    if (modCount != expectedModCount) { // 若是版本號不一致,拋出異常
        throw new ConcurrentModificationException();
    }

    // shift surviving elements left over the spaces left by removed elements
    final boolean anyToRemove = removeCount > 0;
    if (anyToRemove) {
        final int newSize = size - removeCount;
        // 遍歷並剔除要刪除的元素
        for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
            i = removeSet.nextClearBit(i);
            elementData[j] = elementData[i];
        }
        for (int k=newSize; k < size; k++) {
            elementData[k] = null;  // Let gc do its work
        }
        this.size = newSize;
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }
    return anyToRemove;
}

經過源碼咱們能夠知道:

  • ArrayList刪除元素是經過System.arraycopy移動數組覆蓋元素來實現的
  • ArrayList添加元素時沒有校驗null值,因此刪除null值時是特殊處理的
  • ArrayList經過對象刪除時判斷相等是經過equals判斷,因此咱們在儲存自定義對象是要注意對equals進行重寫

經過源碼咱們能夠看出在使用ArrayList時咱們要儘可能避免大量的隨機刪除,由於刪除元素會致使元素拷貝(尤爲是大元素),這是很是消耗性能的一件事;就算咱們經過removeAll()來刪除也不是特別好,由於它也要經過c.contains()去查找元素,不一樣的集合有不一樣的實現方式因此查找的性能也不一樣。

5-4.修改

ArrayList的修改比較簡單,是經過指定索引修改

// 將列表中指定位置的元素替換爲指定的元素。
public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    // 返回被替換的元素
    return oldValue;
}

如今咱們在看看第4問

ArrayList是線程安全的嗎?

經過源碼的閱讀,咱們能夠很輕鬆的回答這個問題。他是不安全的,由於他既沒有在屬性elementDatavalidate,也沒有在方法上加synchronized。並且在ArrayList的類註釋上明確指出他是線程不安全的,要使用線程安全的話可使用Collections.synchronizedList,或者Vector

/* <p><strong>Note that this implementation is not synchronized.</strong>
 * If multiple threads access an <tt>ArrayList</tt> instance concurrently,
 * and at least one of the threads modifies the list structurally, it
 * <i>must</i> be synchronized externally.  (A structural modification is
 * any operation that adds or deletes one or more elements, or explicitly
 * resizes the backing array; merely setting the value of an element is not
 * a structural modification.)  This is typically accomplished by
 * synchronizing on some object that naturally encapsulates the list.
 ***************************************************************************
 * 注意,這個實現是不一樣步。若是多個線程同時訪問ArrayList實例,且至少有一個線程在結構上修改列表,
 * 它必須外部同步。(一個結構修改:添加或刪除一個或多個元素的任何操做,或者是明確的改變數組大小,
 * 僅僅設置元素的值不是結構修改) 這一般是經過在天然封裝列表的對象上同步來實現的。
 
 * If no such object exists, the list should be "wrapped" using the
 * {@link Collections#synchronizedList Collections.synchronizedList}
 * method.  This is best done at creation time, to prevent accidental
 * unsynchronized access to the list:<pre>
 *   List list = Collections.synchronizedList(new ArrayList(...));</pre>
 ***************************************************************************
 * 若是不存在這樣的對象,列表應該使用方法「包裝」(Collections.synchronizedList)。
 * 這最好在建立時進行,以防止意外對列表的非同步訪問*/

至於第5個問題,咱們將在學習LinkedList時在來對比講解。

6.總結

經過上面的學習,咱們已經較爲深入的理解了ArrayList的底層實現,固然若是要很是深入的理解ArrayList確定須要本身親自調試ArrayList的源碼;做爲面試和日常工做,瞭解到這裏也差很少了。

ArrayList本質就是一個能夠自動擴容的數組包裝類,他經過無參構造函數初始化並第一次添加元素的擴容大小默認是10,日後每次自動擴容的大小是原數組容量的1.5倍oldCapacity + (oldCapacity >> 1),在使用ArrayList時儘可能肯定初始化容量的大小,這樣能夠避免頻繁擴容;也要儘可能避免隨機插入和刪除操做,這樣會引發元素移動,消耗資源(尤爲是對移動大元素來講)。

最後咱們在看看ArrayList的一些方法,沒有必要全記住由於我也記不住,只要有個大概印象就行了,在咱們要用的時候再去查找。

  • trimToSize() 調整列表容量爲列表的當前大小
  • ensureCapacity(int minCapacity) 確保列表容量
  • size() 獲取列表元素個數
  • contains(Object o) 判斷是否包含某個對象
  • indexOf(Obejct o) 從前日後查找指定對象
  • lastIndexOf(Obejct o) 從後往前查找指定對象
  • clone() 克隆列表
  • toArray() 轉換爲數組
  • toArray(T[] a) 轉換爲指定類型數組
  • get(int index) 獲取指定索引元素
  • set(int index,E element) 指定索引位置修改
  • add(E o) 向列表末尾添加元素
  • add(int index,E elemet) 指定位置插入元素
  • remove(int index) 移除指定索引
  • remove(Object o) 移除指定元素
  • clear() 狀況列表
  • addAll(Collection<? extends E> c) 在列表末尾添加集合
  • addAll(int index,Collection<? extends E> c) 在列表指定索引添加集合
  • removeAll(Collection<? > c) 移除包含集合內的全部元素
  • retainAll(Collection<? > c) 移除集合內沒有的元素
  • iterator() 返回一個迭代器
  • subList(int fromIndex,int toIndex) 截取子數組
  • forEach(Consumer<? super E> action) 加強for循環
  • removeIf(Predicate<? super E> filter) 刪除元素
  • replaceAll(UnayOperator operator) 替換指定元素
  • sort(Comparator<? super E>) 排序
  • isEmpty() 是否爲空
微信關注【面試情報局】咱們一塊兒幹翻面試官。
相關文章
相關標籤/搜索