自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

萬萬沒想到!!! 谷歌面試原來也問ArrayList

開發(fā) 前端
前幾天H同學(xué)和我聊了下去谷歌的面試經(jīng)驗(yàn),令我詫異的是,沒想到谷歌也問ArrayList???仔細(xì)一想也正常,畢竟集合是Java程序員逃不掉的金光咒。

[[417055]]

本文轉(zhuǎn)載自微信公眾號「稀飯下雪」,作者帥氣的小飯飯 。轉(zhuǎn)載本文請聯(lián)系稀飯下雪公眾號。

前幾天H同學(xué)和我聊了下去谷歌的面試經(jīng)驗(yàn),令我詫異的是,沒想到谷歌也問ArrayList???

仔細(xì)一想也正常,畢竟集合是Java程序員逃不掉的金光咒。

看文章前可以先看看以下幾個問題,如果覺得莫得問題,可以直接跳過該篇文章了,不用浪費(fèi)大家時間。

  • ArrayList使用無參構(gòu)造函數(shù)的時候什么時候進(jìn)行擴(kuò)容?
  • 說說看ArrayList是擴(kuò)容的時候是怎么復(fù)制數(shù)組的?
  • ArrayList遍歷刪除的時候會觸發(fā)什么機(jī)制?為什么用迭代器遍歷刪除不會?

好了,接下來繼續(xù)聊聊高頻面試題 ArrayList。

ArrayList的擴(kuò)容機(jī)制

  1. // 存儲數(shù)組元素的緩沖區(qū) 
  2. transient Object[] elementData; 
  3. // 默認(rèn)空數(shù)組元素 
  4. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 
  5. // 默認(rèn)初始化容量 
  6. private static final int DEFAULT_CAPACITY = 10; 
  7. // 數(shù)組的大小 
  8. private int size
  9. // 記錄被修改的次數(shù) 
  10. protected transient int modCount = 0; 
  11. // 數(shù)組的最大值 
  12. private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 

底層ArrayList使用數(shù)組實(shí)現(xiàn),不設(shè)置的話,默認(rèn)初始容量為10

  1. // 數(shù)組擴(kuò)容方法 
  2. // minCapacity = size + 1 
  3. private int newCapacity(int minCapacity) { 
  4.     // 當(dāng)前數(shù)組長度 
  5.     int oldCapacity = elementData.length; 
  6.     // 新的數(shù)組容量 = 舊數(shù)組長度 + 舊數(shù)組長度 / 2  
  7.     // oldCapacity = 10   oldCapacity >> 1   --- 5 
  8.     // 例如10的二進(jìn)制為 :  0000 1010 >> 1  ----->  0000 0101 = 5 
  9.     int newCapacity = oldCapacity + (oldCapacity >> 1); 
  10.     if (newCapacity - minCapacity <= 0) { 
  11.         if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) 
  12.             // 如果一開始沒有定義初始容量這時newCapacity=0,返回默認(rèn)容量10 
  13.             // 可以得出當(dāng)無參new 一個ArrayList()時候,這個ArrayList()為空集合,size為0 
  14.             return Math.max(DEFAULT_CAPACITY, minCapacity); 
  15.         if (minCapacity < 0) // overflow 
  16.             throw new OutOfMemoryError(); 
  17.         return minCapacity; 
  18.     } 
  19.     return (newCapacity - MAX_ARRAY_SIZE <= 0) 
  20.         ? newCapacity    // 這里返回的長度為原數(shù)組的1.5倍 
  21.         : hugeCapacity(minCapacity); 

當(dāng)增加元素的時候發(fā)現(xiàn)底層數(shù)組的需要的容量(size+1)大于數(shù)組的容量的時候,就會觸發(fā)擴(kuò)容,在首次調(diào)用add()方法之后,返回一個容量為10的數(shù)組,后面每次擴(kuò)容后新數(shù)組的長度為原數(shù)組長度的 「1.5」 倍,并調(diào)用底層原生的System.arraycopy將舊數(shù)組的數(shù)據(jù)copy到新的數(shù)組中,完成整個擴(kuò)容。

所以日常開發(fā)中,在知道初始值的時候先設(shè)置初始值,因?yàn)閿U(kuò)容是比較耗性能的。

「不用腦子的總結(jié):首次擴(kuò)容為10 ,后面每次擴(kuò)容為原數(shù)組的1.5倍,調(diào)用底層原生的System.arraycopy將舊數(shù)組的數(shù)據(jù)copy到新的數(shù)組中,完成整個擴(kuò)容?!?/p>

ArrayList添加元素與擴(kuò)容

ArrayList.add(E e)源碼:

  1. public boolean add(E e) { 
  2.     ensureCapacityInternal(size + 1);  // Increments modCount!! 
  3.     elementData[size++] = e; 
  4.     return true

add()中elementData[size++] = e很好理解,就是將元素插入第size個位置,然后將size++,我們重點(diǎn)來看看ensureCapacityInternal(size + 1)方法;

  1. private void ensureCapacityInternal(int minCapacity) { 
  2.     if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 
  3.         minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); 
  4.     } 
  5.     ensureExplicitCapacity(minCapacity); 

ensureCapacityInternal()方法中判斷緩存變量elementData是否為空,也就是判斷是否是第一次添加元素,如果是第一次添加元素,則設(shè)置初始化大小為默認(rèn)容量10,否則為傳入的參數(shù)。這個方法的目的就是「獲取初始化數(shù)組容量」。獲取到初始化容量后調(diào)用ensureExplicitCapacity(minCapacity)方法;

  1. private void ensureExplicitCapacity(int minCapacity) { 
  2.     modCount++; 
  3.  
  4.     // overflow-conscious code 
  5.     if (minCapacity - elementData.length > 0) 
  6.         grow(minCapacity); 

ensureExplicitCapacity(minCapacity)方法用來判斷是否需要擴(kuò)容,假如第一次添加元素,minCapacity為10,elementData容量為0,那么就需要去擴(kuò)容。調(diào)用grow(minCapacity)方法。

  1. // 數(shù)組的最大容量 
  2. private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 
  3.  
  4. private void grow(int minCapacity) { 
  5.     // overflow-conscious code 
  6.     int oldCapacity = elementData.length; 
  7.     // 擴(kuò)容大小為原來數(shù)組長度的1.5倍 
  8.     int newCapacity = oldCapacity + (oldCapacity >> 1); 
  9.     // 擴(kuò)容容量比需要擴(kuò)容的長度小,則使用需要擴(kuò)容的容量 
  10.     if (newCapacity - minCapacity < 0) 
  11.         newCapacity = minCapacity; 
  12.     // 擴(kuò)容容量比最大數(shù)組長度大,則使用最大整數(shù)長度 
  13.     if (newCapacity - MAX_ARRAY_SIZE > 0) 
  14.         newCapacity = hugeCapacity(minCapacity); 
  15.     // minCapacity is usually close to size, so this is a win: 
  16.     elementData = Arrays.copyOf(elementData, newCapacity); 

grow(minCapacity)方法對數(shù)組進(jìn)行擴(kuò)容,擴(kuò)容大小為原數(shù)組的1.5倍,如果計(jì)算出的擴(kuò)容容量比需要的容量小,則擴(kuò)容大小為需要的容量,可以看到,第一次擴(kuò)容的時候其實(shí)是10。如果擴(kuò)容容量比數(shù)組最大容量大,則調(diào)用hugeCapacity(minCapacity)方法,將數(shù)組擴(kuò)容為整數(shù)的最大長度,然后將elemetData數(shù)組指向新擴(kuò)容的內(nèi)存空間并將元素復(fù)制到新空間,這里使用的是 Arrays.copyOf(elementData, newCapacity)

  1. public static int[] copyOf(int[] original, int newLength) { 
  2.     int[] copy = new int[newLength]; 
  3.     System.arraycopy(original, 0, copy, 0, 
  4.                      Math.min(original.length, newLength)); 
  5.     return copy; 

可以看到底層使用的是System.arraycopy,而這個copy的過程是比較耗性能的,因此建議初始化時預(yù)估一個容量大小。

「不用腦子的總結(jié):用無參構(gòu)造函數(shù)創(chuàng)建ArrayList后進(jìn)行第一次擴(kuò)容容量是10,后續(xù)則是1.5倍,底層調(diào)用的是System.arraycopy,而這個copy的過程是比較耗性能的,因此建議初始化時預(yù)估一個容量大小。」

ArrayList刪除元素

ArrayList提供兩種刪除元素的方法,可以通過索引和元素進(jìn)行刪除。兩種刪除大同小異,刪除元素后,將后面的元素一次向前移動。

ArrayList.remove(int index)源碼:

  1. public E remove(int index) { 
  2.     rangeCheck(index); 
  3.  
  4.     modCount++; 
  5.     E oldValue = elementData(index); 
  6.  
  7.     int numMoved = size - index - 1; 
  8.     if (numMoved > 0) 
  9.         System.arraycopy(elementData, index+1, elementData, index
  10.                          numMoved); 
  11.     elementData[--size] = null; // clear to let GC do its work 
  12.  
  13.     return oldValue; 

刪除元素時,首先會判斷索引是否大于ArrayList的大小,如果索引范圍正確,則將索引位置的下一個元素賦值到索引位置,將ArrayList的大小-1,最后返回移除的元素。

「不用腦子的總結(jié):刪除后底層調(diào)用的依舊是System.arraycopy,而這個copy的過程是比較耗性能的,因此才說頻繁增刪的盡量別用ArrayList?!?/p>

ArrayList遍歷刪除

  1. @Override 
  2. public void forEach(Consumer<? super E> action) { 
  3.     Objects.requireNonNull(action); 
  4.     // 預(yù)設(shè)值了一個expectedModCount值 
  5.     final int expectedModCount = modCount; 
  6.     @SuppressWarnings("unchecked"
  7.     final E[] elementData = (E[]) this.elementData; 
  8.     final int size = this.size
  9.     // 遍歷過程中拿出來判斷 
  10.     for (int i=0; modCount == expectedModCount && i < size; i++) { 
  11.         action.accept(elementData[i]); 
  12.     } 
  13.     // 如果對不上則報錯 
  14.     if (modCount != expectedModCount) { 
  15.         throw new ConcurrentModificationException(); 
  16.     } 
  1. public E remove(int index) {  
  2.     rangeCheck(index);  
  3.  // 修改了modCount  
  4.     modCount++;  
  5.     E oldValue = elementData(index);  
  6.   
  7.     int numMoved = size - index - 1;  
  8.     if (numMoved > 0)  
  9.         System.arraycopy(elementData, index+1, elementData, index,  
  10.                          numMoved);  
  11.     elementData[--size] = null; // clear to let GC do its work  
  12.   
  13.     return oldValue;  
  14. }  

從代碼就可以看出來了,在遍歷的時候會率先 預(yù)設(shè)值了一個expectedModCount值,然后再遍歷拿出來判斷,如果不一樣了,則中斷流程并且報錯,而這個過程則涉及到了快速失敗機(jī)制了,正常來說,ArrayList不允許遍歷刪除。

「不用腦子的總結(jié):ArrayList通過預(yù)設(shè)值expectedModCount實(shí)現(xiàn)了快速失敗機(jī)制,避免了多線程遍歷刪除或者增加,以及遍歷過程中增刪元素?!?/p>

集合的快速失敗(fail-fast)

它是 Java 集合的一種錯誤檢測機(jī)制,當(dāng)多個線程對集合進(jìn)行結(jié)構(gòu)上的改變操作時,有可能會產(chǎn)生 fail-fast 機(jī)制。

迭代器在遍歷時直接訪問集合中的內(nèi)容,并且在遍歷過程中使用一個 modCount 變量。集合在被遍歷期間如果內(nèi)容發(fā)生變化,就會改變modCount的值。每當(dāng)?shù)魇褂胔ashNext()/next()遍歷下一個元素之前,都會檢測modCount變量是否為expectedmodCount值,是的話就返回遍歷;否則拋出異常,終止遍歷。

注意:這里異常的拋出條件是檢測到 modCount!=expectedmodCount 這個條件。如果集合發(fā)生變化時修改modCount值剛好又設(shè)置為了expectedmodCount值,則異常不會拋出。因此,不能依賴于這個異常是否拋出而進(jìn)行并發(fā)操作的編程,這個異常只建議用于檢測并發(fā)修改的bug。

場景:java.util包下的集合類都是快速失敗的,不能在多線程下發(fā)生并發(fā)修改(迭代過程中被修改)。

「不用腦子的總結(jié):我們?nèi)粘?吹降腃oncurrent Modification Exception,其實(shí)就是觸發(fā)了快速失敗機(jī)制的表現(xiàn),做法也很簡單:在遍歷的時候給你給modCount設(shè)置個備份expectedModCount,如果有多線程在搞,那么必定會導(dǎo)致modCount被改,那么就容易了,每次遍歷的時候都檢測下modCount變量是否為expectedModCount就可以了,如果不是意味著被改了,那我就不管,我就要報錯?!?/p>

集合的安全失敗(fail-safe)

采用安全失敗機(jī)制的集合容器,在遍歷時不是直接在集合內(nèi)容上訪問的,而是先復(fù)制原有集合內(nèi)容,在拷貝的集合上進(jìn)行遍歷。

原理:由于迭代時是對原集合的拷貝進(jìn)行遍歷,所以在遍歷過程中對原集合所作的修改并不能被迭代器檢測到,所以不會觸發(fā)Concurrent Modification Exception。

缺點(diǎn):基于拷貝內(nèi)容的優(yōu)點(diǎn)是避免了Concurrent Modification Exception,但同樣地,迭代器并不能訪問到修改后的內(nèi)容,即:迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發(fā)生的修改迭代器是不知道的。

場景:java.util.concurrent包下的容器都是安全失敗,可以在多線程下并發(fā)使用,并發(fā)修改。

「不用記憶的總結(jié):那么為啥并發(fā)容器的時候不怕呢?簡單,因?yàn)椴捎昧税踩C(jī)制,在遍歷的時候直接拷貝了一份出來,這樣就不會觸發(fā)了?!?/p>

使用ArrayList的subList()需要注意的地方

  1. public List<E> subList(int fromIndex, int toIndex) { 
  2.         subListRangeCheck(fromIndex, toIndex, size); 
  3.         return new SubList(this, 0, fromIndex, toIndex); 
  1. SubList(AbstractList<E> parent,int offset,int fromIndex,int toIndex) { 
  2.             this.parent = parent; 
  3.             this.parentOffset = fromIndex; 
  4.             this.offset = offset + fromIndex; 
  5.             this.size = toIndex - fromIndex; 
  6.             this.modCount = ArrayList.this.modCount; 

subList()返回結(jié)果不可強(qiáng)制轉(zhuǎn)為ArrayList類型,因?yàn)樵摲椒▽?shí)質(zhì)是創(chuàng)建一個內(nèi)部類SubList實(shí)例,這個SubList是AbstractList的實(shí)現(xiàn)類,并不繼承于ArrayList。

通過上面源碼可以看出,通過parent屬性指定父類并直接引用了原有的List,并返回該父類的部分視圖,只是指定了他要使用的元素的范圍fromIndex(包含),endIndex(不包含)。

那么,如果對其原有或者子List做數(shù)據(jù)性修改,則會互相影響。如果對原有List進(jìn)行結(jié)構(gòu)性修改,則會踩坑Fast-fail,報錯會拋出異常ConcurrentModification Exception。

ArrayList迭代器

看下迭代器的遍歷和刪除相關(guān)的源碼

  1. public boolean hasNext() { 
  2.     return cursor != size
  3.  
  4. @SuppressWarnings("unchecked"
  5. public E next() { 
  6.     // 同樣判斷modCount != expectedModCount,不同則報錯 
  7.     checkForComodification(); 
  8.     int i = cursor
  9.     if (i >= size
  10.         throw new NoSuchElementException(); 
  11.     Object[] elementData = ArrayList.this.elementData; 
  12.     if (i >= elementData.length) 
  13.         throw new ConcurrentModificationException(); 
  14.     cursor = i + 1; 
  15.     return (E) elementData[lastRet = i]; 
  16.  
  17. public void remove() { 
  18.     if (lastRet < 0) 
  19.         throw new IllegalStateException(); 
  20.     checkForComodification(); 
  21.  
  22.     try { 
  23.         ArrayList.this.remove(lastRet); 
  24.         cursor = lastRet; 
  25.         lastRet = -1; 
  26.         // 這里刪除后會重新復(fù)制一次 
  27.         expectedModCount = modCount; 
  28.     } catch (IndexOutOfBoundsException ex) { 
  29.         throw new ConcurrentModificationException(); 
  30.     } 

通過代碼我們也可以看出ArrayList的迭代器是支持遍歷刪除的,因?yàn)樵趧h除后會重新賦一次值給expectedModCount。

ArrayList和LinkedList的優(yōu)劣

其實(shí)就是數(shù)組和鏈表的優(yōu)劣勢,ArrayList優(yōu)點(diǎn),支持隨機(jī)訪問,get(i)的時間復(fù)雜度為O(1),而缺點(diǎn)就是需要擴(kuò)容,要復(fù)制數(shù)組,而且內(nèi)部插入數(shù)據(jù)需要移動數(shù)據(jù),插入刪除的性能差;

對于LinkedList來說,優(yōu)點(diǎn)就是容量理論上來說是無限,不存在擴(kuò)容,而且可以很方便的插入和刪除數(shù)據(jù)(性能損失在查找),而缺點(diǎn)就是不能隨機(jī)訪問,get(i)需要遍歷。 

貌似就是反過來的,所以在實(shí)際開發(fā)中也很容易區(qū)別,看是查找頻繁、還是增刪頻繁,如果是查找頻繁就用ArrayList,如果增刪頻繁就用LinkedList即可。

 

責(zé)任編輯:武曉燕 來源: 稀飯下雪
相關(guān)推薦

2021-11-29 05:37:24

Windows Def操作系統(tǒng)微軟

2015-07-15 13:00:31

英特爾開源

2023-10-31 12:29:25

模型訓(xùn)練

2017-12-12 11:09:39

顯卡散熱CPU

2018-06-27 14:23:38

機(jī)器學(xué)習(xí)人工智能入門方法

2018-05-02 09:38:02

程序員代碼互聯(lián)網(wǎng)

2024-01-04 12:33:17

ChatGPTAI視頻

2021-02-21 17:14:27

程序員技能開發(fā)者

2020-06-08 08:38:24

可執(zhí)行文件文件字符

2021-08-31 09:35:01

TCPIP漏洞

2023-08-10 08:00:00

2019-08-19 09:21:36

程序員Bug代碼

2021-07-21 05:38:20

中國聯(lián)通攜號轉(zhuǎn)網(wǎng)移動

2021-01-27 18:13:35

日志nginx信息

2019-04-12 09:24:46

Spring Clou服務(wù)注冊

2019-04-28 14:14:48

爬蟲網(wǎng)絡(luò)特價機(jī)票

2019-10-12 08:53:26

Redis多線程版本

2017-12-26 15:41:26

2018-01-26 23:23:23

JDBC MySQL數(shù)據(jù)庫

2019-12-09 10:13:20

HashMap選擇容量
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號