五道ArrayList面試題


ArrayList概述

(1)ArrayList 是一種變長的集合類,基於定長數組實現。

(2)ArrayList 允許空值和重復元素,當往 ArrayList 中添加的元素數量大於其底層數組容量時,其會通過擴容機制重新生成一個更大的數組。

(3)ArrayList 底層基於數組實現,所以其可以保證在 O(1) 復雜度下完成隨機查找操作。

(4)ArrayList 是非線程安全類,並發環境下,多個線程同時操作 ArrayList,會引發不可預知的異常或錯誤。

ArrayList的成員屬性

在介紹關於ArrayList的各種方法之前先看一下基礎屬性成員。其中DEFAULTCAPACITY_EMPTY_ELEMENTDATA與EMPTY_ELEMENTDATA的區別是:當我們向數組中添加第一個元素時,DEFAULTCAPACITY_EMPTY_ELEMENTDATA將會知道數組該擴充多少。

//默認初始化容量 private static final int DEFAULT_CAPACITY = 10; //默認的空的數組,這個主要是在構造方法初始化一個空數組的時候使用 private static final Object[] EMPTY_ELEMENTDATA = {}; //使用默認size大小的空數組實例,和EMPTY_ELEMENTDATA區分開來, //這樣可以知道當第一個元素添加的時候進行擴容至多少 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList底層存儲數據就是通過數組的形式,ArrayList長度就是數組的長度。 //一個空的實例elementData為上面的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,當添加第一個元素的時候 //會進行擴容,擴容大小就是上面的默認容量DEFAULT_CAPACITY transient Object[] elementData; // non-private to simplify nested class access //arrayList的大小 private int size;

static修飾的EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA

ArrayList構造方法

(1)帶有初始化容量的構造方法

  • 參數大於0,elementData初始化為initialCapacity大小的數組
  • 參數小於0,elementData初始化為空數組
  • 參數小於0,拋出異常
//參數為初始化容量 public ArrayList(int initialCapacity) { //判斷容量的合法性 if (initialCapacity > 0) { //elementData才是實際存放元素的數組 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //如果傳遞的長度為0,就是直接使用自己已經定義的成員變量(一個空數組) this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }

(2)無參構造

  • 構造方法中將elementData初始化為空數組DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • 當調用add方法添加第一個元素的時候,會進行擴容
  • 擴容至大小為DEFAULT_CAPACITY=10
//無參構造,使用默認的size為10的空數組,在構造方法中沒有對數組長度進行設置,會在后續調用add方法的時候進行擴容 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }

(3)參數為Collection類型的構造器

//將一個參數為Collection的集合轉變為ArrayList(實際上就是將集合中的元素換為了數組的形式)。如果 //傳入的集合為null會拋出空指針異常(調用c.toArray()方法的時候) public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { //c.toArray()可能不會正確地返回一個 Object[]數組,那么使用Arrays.copyOf()方法 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { //如果集合轉換為數組之后數組長度為0,就直接使用自己的空成員變量初始化elementData this.elementData = EMPTY_ELEMENTDATA; } }

​ 上面的這些構造方法理解起來比較簡單,關注前兩個構造方法做的事情,目的都是初始化底層數組 elementData(this.elementData=XXX)。區別在於無參構造方法會將 elementData 初始化一個空數組,插入元素時,擴容將會按默認值重新初始化數組。而有參的構造方法則會將 elementData 初始化為參數值大小(>= 0)的數組。一般情況下,我們用默認的構造方法即可。倘若在可知道將會向 ArrayList 插入多少元素的情況下,可以使用有參構造方法。

​ 上面說到了使用無參構造的時候,在調用add方法的時候會進行擴容,所以下面我們就看看add方法以及擴容的細節

ArrayList的add方法

add方法大致流程

//將指定元素添加到list的末尾 public boolean add(E e) { //因為要添加元素,所以添加之后可能導致容量不夠,所以需要在添加之前進行判斷(擴容) ensureCapacityInternal(size + 1); // Increments modCount!!(待會會介紹到fast-fail) elementData[size++] = e; return true; }

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

ensureCapacityInternal方法分析

private void ensureCapacityInternal(int minCapacity) { //這里就是判斷elementData數組是不是為空數組 //(使用的無參構造的時候,elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA) //如果是,那么比較size+1(第一次調用add的時候size+1=1)和DEFAULT_CAPACITY, //那么顯然容量為10 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }

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

ensureExplicitCapacity方法分析

private void ensureExplicitCapacity(int minCapacity) { modCount++; //這里就是add方法中注釋的Increments modCount //溢出 if (minCapacity - elementData.length > 0) grow(minCapacity);//這里就是執行擴容的方法 }

​ 下面來看一下擴容的主要方法grow。

grow方法分析

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private void grow(int minCapacity) { // oldCapacity為舊數組的容量 int oldCapacity = elementData.length; // newCapacity為新數組的容量(oldCap+oldCap/2:即更新為舊容量的1.5倍) int newCapacity = oldCapacity + (oldCapacity >> 1); // 檢查新容量的大小是否小於最小需要容量,如果小於那舊將最小容量最為數組的新容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //如果新容量大於MAX_ARRAY_SIZE,使用hugeCapacity比較二者 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); }

hugeCapacity方法

這里簡單看一下hugeCapacity方法

private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); //對minCapacity和MAX_ARRAY_SIZE進行比較 //若minCapacity大,將Integer.MAX_VALUE作為新數組的大小 //若MAX_ARRAY_SIZE大,將MAX_ARRAY_SIZE作為新數組的大小 //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }

add方法執行流程總結

​ 我們用一幅圖來簡單梳理一下,當使用無參構造的時候,在第一次調用add方法之后的執行流程

​ 這是第一次調用add方法的過程,當擴容值capacity為10之后,

  • 繼續添加第2個元素(先注意調用ensureCapacityInternal方法傳遞的參數為size+1=1+1=2)

  • 在ensureCapacityInternal方法中,elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA不成立,所以直接執行ensureExplicitCapacity方法
  • ensureExplicitCapacity方法中minCapacity為剛剛傳遞的2,所以第二個if判斷(2-10=-8)不會成立,即newCapacity 不比 MAX_ARRAY_SIZE大,則不會進入 grow 方法。數組容量為10,add方法中 return true,size增為1。
  • 假設又添加3、4......10個元素(其中過程類似,但是不會執行grow擴容方法)
  • 當add第11個元素時候,會進入grow方法時,計算newCapacity為15,比minCapacity(為10+1=11)大,第一個if判斷不成立。新容量沒有大於數組最大size,不會進入hugeCapacity方法。數組容量擴為15,add方法中return true,size增為11。

add(int index,E element)方法

//在元素序列 index 位置處插入 public void add(int index, E element) { rangeCheckForAdd(index); //校驗傳遞的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) { if (index > size || index < 0) //這里判斷的index>size(保證數組的連續性),index小於0 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }

add(int index, E element)方法(在元素序列指定位置(假設該位置合理)插入)的過程大概是下面這些

  1. 檢測數組是否有足夠的空間(這里的實現和上面的)
  2. 將 index 及其之后的所有元素向后移一位
  3. 將新元素插入至 index 處.

將新元素插入至序列指定位置,需要先將該位置及其之后的元素都向后移動一位,為新元素騰出位置。這個操作的時間復雜度為O(N),頻繁移動元素可能會導致效率問題,特別是集合中元素數量較多時。在日常開發中,若非所需,我們應當盡量避免在大集合中調用第二個插入方法。

ArrayList的remove方法

ArrayList支持兩種刪除元素的方式

1、remove(int index) 按照下標刪除

public E remove(int index) { rangeCheck(index); //校驗下標是否合法(如果index>size,舊拋出IndexOutOfBoundsException異常) modCount++;//修改list結構,就需要更新這個值 E oldValue = elementData(index); //直接在數組中查找這個值 int numMoved = size - index - 1;//這里計算所需要移動的數目 //如果這個值大於0 說明后續有元素需要左移(size=index+1) //如果是0說明被移除的對象就是最后一位元素(不需要移動別的元素) if (numMoved > 0) //索引index只有的所有元素左移一位 覆蓋掉index位置上的元素 System.arraycopy(elementData, index+1, elementData, index, numMoved); //移動之后,原數組中size位置null elementData[--size] = null; // clear to let GC do its work //返回舊值 return oldValue; } //src:源數組 //srcPos:從源數組的srcPos位置處開始移動 //dest:目標數組 //desPos:源數組的srcPos位置處開始移動的元素,這些元素從目標數組的desPos處開始填充 //length:移動源數組的長度 public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

​ 刪除過程如下圖所示

2、remove(Object o) 按照元素刪除,會刪除和參數匹配的第一個元素

public boolean remove(Object o) { //如果元素是null 遍歷數組移除第一個null if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { //遍歷找到第一個null元素的下標 調用下標移除元素的方法 fastRemove(index); return true; } } else { //找到元素對應的下標 調用下標移除元素的方法 for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { 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 }

ArrayList的其他方法

ensureCapacity方法

最好在 add 大量元素之前用 ensureCapacity 方法,以減少增量從新分配的次數

public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } }

ArrayList總結

(1)ArrayList 是一種變長的集合類,基於定長數組實現,使用默認構造方法初始化出來的容量是10(1.7之后都是延遲初始化,即第一次調用add方法添加元素的時候才將elementData容量初始化為10)。

(2)ArrayList 允許空值和重復元素,當往 ArrayList 中添加的元素數量大於其底層數組容量時,其會通過擴容機制重新生成一個更大的數組。ArrayList擴容的長度是原長度的1.5倍

(3)由於 ArrayList 底層基於數組實現,所以其可以保證在 O(1) 復雜度下完成隨機查找操作。

(4)ArrayList 是非線程安全類,並發環境下,多個線程同時操作 ArrayList,會引發不可預知的異常或錯誤。

(5)順序添加很方便

(6)刪除和插入需要復制數組,性能差(可以使用LinkindList)

(7)Integer.MAX_VALUE - 8 :主要是考慮到不同的JVM,有的JVM會在加入一些數據頭,當擴容后的容量大於MAX_ARRAY_SIZE,我們會去比較最小需要容量和MAX_ARRAY_SIZE做比較,如果比它大, 只能取Integer.MAX_VALUE,否則是Integer.MAX_VALUE -8。 這個是從jdk1.7開始才有的

fast-fail機制

fail-fast的解釋:

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system’s state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

​ 大概意思是:在系統設計中,快速失效系統一種可以立即報告任何可能表明故障的情況的系統。快速失效系統通常設計用於停止正常操作,而不是試圖繼續可能存在缺陷的過程。這種設計通常會在操作中的多個點檢查系統的狀態,因此可以及早檢測到任何故障。快速失敗模塊的職責是檢測錯誤,然后讓系統的下一個最高級別處理錯誤。

​ 其實就是在做系統設計的時候先考慮異常情況,一旦發生異常,直接停止並上報,比如下面的這個簡單的例子

//這里的代碼是一個對兩個整數做除法的方法,在fast_fail_method方法中,我們對被除數做了個簡單的檢查,如果其值為0,那么就直接拋出一個異常,並明確提示異常原因。這其實就是fail-fast理念的實際應用。 public int fast_fail_method(int arg1,int arg2){ if(arg2 == 0){ throw new RuntimeException("can't be zero"); } return arg1/arg2; }

​ 在Java集合類中很多地方都用到了該機制進行設計,一旦使用不當,觸發fail-fast機制設計的代碼,就會發生非預期情況。我們通常說的Java中的fail-fast機制,默認指的是Java集合的一種錯誤檢測機制。當多個線程對部分集合進行結構上的改變的操作時,有可能會觸發該機制時,之后就會拋出並發修改異常ConcurrentModificationException.當然如果不在多線程環境下,如果在foreach遍歷的時候使用add/remove方法,也可能會拋出該異常。參考fast-fail機制,這里簡單做個總結

之所以會拋出ConcurrentModificationException異常,是因為我們的代碼中使用了增強for循環,而在增強for循環中,集合遍歷是通過iterator進行的,但是元素的add/remove卻是直接使用的集合類自己的方法。這就導致iterator在遍歷的時候,會發現有一個元素在自己不知不覺的情況下就被刪除/添加了,就會拋出一個異常,用來提示可能發生了並發修改!所以,在使用Java的集合類的時候,如果發生ConcurrentModificationException,優先考慮fail-fast有關的情況,實際上這可能並沒有真的發生並發,只是Iterator使用了fail-fast的保護機制,只要他發現有某一次修改是未經過自己進行的,那么就會拋出異常。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM