本文靈感來源於:羣友遍歷列表時remove元素引起異常,後對for循環的實現原理進行一系列的探究~java
Java:先判斷是否知足條件 → 執行循環體 → 自增
Kotlin:遍歷的是範圍,直接進循環體編程
modCount → 記錄列表結構修改次數,調用列表的remove方法,此值+1
expectedModCount → 預計修改次數,調用Itr迭代器中的remove方法時,會調用列表的remove方法,然後將modCount賦值給expectedModCount,即保證遍歷過程當中兩個值是相等的;
若是此時調用列表的remove方法,modCount增長1,而迭代器中的expectedModCount沒變,二者不等,就會引起ConcurrentModificationException異常。設計模式
作系統設計的時候先考慮異常狀況,一旦發生異常,直接中止並上報,一種用於提早預警的Bug檢測機制;在集合中的應用就是:在遍歷一個集合時,當集合結構被修改,直接拋出異常。數組
- 單線程:使用迭代器進行remove;
- 多線程:在使用迭代器處加鎖(如synchronize);
- 使用fail-safe(安全失敗)機制的同步容器,如CopyOnWriteArrayList,在java.util.concurrent包中;
建立了新的ArrayList用做遍歷,remove移除的是舊ArrayList的元素,故互不影響緩存
寒冷的午後,在一個交流羣裏,一位開發者盆友拋出了下面的問題:安全
同時附帶兩張截圖:多線程
恰好在寫代碼的我,隨手點開了 toList() 的源碼:併發
慣性思惟 回了句:涉及到可變列表和不可變列表吧app
接着截了個 Collection.kt 的文件結構圖後,追加:函數式編程
其實我並無理解他的問題,就開始跟起了RecyclerView的源碼,再經歷過一番排查得出:
應該和 Iterable,普通for循環,加強for循環有關
而後被拉去開了個兩個半小時的用例評審…回來看到問題還沒解決,看了聊天記錄,捋了一下才弄懂他的問題:
一、普通for循環(下標遍歷),相似的代碼,Java不報錯,Kotlin卻拋 IndexOutOfBoundsException;
二、加強for循環(foreach),remove移除,都會拋 ConcurrentModificationException
看不懂?寫個簡單的代碼演示下問題:
① Java
② Kotlin
初始化列表:
行吧,接着一個個講解~
緣由其實很簡單「循環條件不同」,Java中是:
先判斷是否知足條件 → 執行循環體 → 自增 ,打斷點跟下i、ls.size(),記錄以下:
0→五、1→四、2→三、3(此時size=3,判斷條件不成立,不會自增,走循環體),因此上面看到只打印了3次。
而Kotlin則不同,點開 for(i in l.indices) 裏的indices:
噢,indices是一個擴展屬性,值爲:0..size - 1,一個範圍(Range),好比如今有5個元素,這個值就是:0..(5-1) → 0..4,你能夠簡單地把Range當作一個List(列表),那麼這裏的 循環條件 就變成了:遍歷這個範圍,因此
0→五、1→四、2→三、3→3(這裏是 直接進循環體,數組長度爲3,訪問3個,因此就引起了異常!)
到此:普通for循環的「血案(數組越界)」就此水落石出,接着看下加強for循環~
有讀者可能有疑問,上述的Kotlin代碼用的是 forEach 擴展方法,加強for循環不是應該這樣寫嗎?
其實,forEach也是用的加強for循環,點開源碼就知道了:
在加強for循環的基礎上,傳入一個處理函數,不瞭解函數式編程的可移步至:《八、Kotlin高階函數與lambda表達式》
反編譯下加強for循環字節碼,以下:
逐行源碼解析:
- Iterator var → 定義迭代器類型的臨時變量var1
- l.iterator() → 獲取列表l的迭代器
- var1.hasNext() → 判斷迭代器中是否有未遍歷的元素
- Integer i = (Integer)var1.next() → 獲取第一個未遍歷元素,賦值給臨時變量i
- l.remove(i) → 移除列表中的i元素
不難看出底層是:while循環 + Iterator(迭代器) 。
設計模式中有一種模式「迭代器模式」
打開 Collection.kt,文件結構以下:
這裏要對「Iterable」和「Iterator」進行區分:
Iterable接口:實現此接口的集合對象支持迭代(可配合foreach使用),定義了一個iterator()函數,返回一個Iterator迭代器對象。
Iterator:迭代器,提供迭代機制的對象,代碼以下:
兩個函數:next() => 返回當前迭代元素;hasNext() => next前調用此方法判斷是否迭代到終點
能夠跟下實現看看:ListIterator → AbstractList:
跟IteratorImpl:
以上就是 迭代器的設計「哲學」 的簡單講解。
回到加強for循環中remove元素引發的「ConcurrentModificationException」,此異常直接翻譯:併發修改異常。
可是問題是:咱們並無用多線程修改集合,卻引發這樣的異常?跟一波異常,定位到:
java.util.ArrayList$Itr.checkForComodification
直譯下變量名:modCount(修改次數),expectedModCount(預計修改次數),em…這是判斷兩值不等就直接拋異常?往上看:
modCount賦值給expectedModCount,說明二者一開始是相等的,那modCount是在哪裏賦初值的呢?
點進去來到ArrayList的父類AbstractList
Tips:transient關鍵字 用來標記的成員變量不參與序列化過程)
在AbstractList中搜了下modCount,沒發現有值變化的操做;
回到ArrayList,觀察源碼能夠發現「在涉及結構變化的方法中,modCount都會自增1」
而add(),addAll() 方法雖然沒有明寫modCount++,但暗地裏調用ensureCapacityInternal()完成自增:
到此,咱們知道了modCount →「會在ArrayList的結構發生變化時變化」
接着,到expectedModCount,定位到內部類 Itr:
類的組成比較簡單:實現了Iterator迭代器接口,重寫next()和hasNext()方法;定義了兩個變量:cursor(下一個元素的下標),lastRet(上一個元素的下標);讀者有疑問的多是next()裏的:ArrayList.this.elementData,跟一下:
噢,就是一個緩存數組,長度爲列表的容量,當第一個元素添加進來,會擴展至DEFAULT_CAPACITY:
看回remove()方法,調用ArrayList.this.remove()移除元素後,把modCount的賦值給expectedModCount;
繼續往下走,由於此時二者相等,hasNext()執行checkForComodification()沒拋出異常,按流程走是沒問題的。
但:咱們上面跳過了迭代器,直接對列表進行了remove,而此時modCount已經+1,但expectedModCount沒變,因此執行到checkForComodification就拋出異常了!
嗯,你有個大膽的想法?直接用迭代器進行remove會拋異常嗎?那就試試:
行吧,用迭代器的方式remove,但此時已經不是加強for循環了,哈哈!
從上面咱們知道了異常發生的緣由了,那爲什麼要這樣設計呢?這種玩法有個專業名詞 →「fail-fast(快速失敗)機制」
在作系統設計的時候先考慮異常狀況,一旦發生異常,直接中止並上報。
舉個簡單的例子,寫個兩數相除的方法,若是不當心除以0,運行時就會引起異常ArithmeticException by/zero,而及時失敗,則是在執行運算前,檢測被除數是否爲0,是直接拋出異常。示例以下:
行吧,一種檢測Bug機制,用於提早預警,那ArrayList裏爲什麼要用到這種機制呢?
→ 由於ArrayList不是線程安全的,多個線程同時訪問同一個ArrayList可能會引發異常;
→ 引入及時失敗是想着:若某個線程經過Iterator遍歷時,集合內容被其餘線程改變,拋出異常;
在上面的例子中,咱們利用迭代器遍歷remove的方式來規避ConcurrentModificationException,
但那是隻有在單個線程訪問列表,若是是有多個線程在訪問呢?寫個例子試試:
行吧,拋異常了,那有在多線程的時候有什麼辦法規避呢?換個線程安全的容器,好比:Vector?
同樣會拋異常,Vector雖然採用synchronized進行同步,但仍是繼承自AbstarctList,直接經過Iterator便可訪問容器元素,根本不須要獲取鎖。
哪有解決方法嗎?
有,直接在使用iterator處加鎖便可,代碼示例以下:
能夠,那除了對iterator加鎖的方式外,還有其餘方法嗎?
有,上併發容器,它採用的是fail-safe(安全失敗)機制:迭代時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷,遍歷期間原集合發生的修改迭代器是不知道的,以此避免了ConcurrentModificationException,但一樣的迭代器不能訪問到修改後的內容。Java中的併發容器放在java.util.concurrent包中,好比可使用CopyOnWriteArrayList來代替ArrayList和Vector。
差點漏了,那位盤友問的爲啥加個toList()就能夠了,代碼示例以下:
跟下源碼:
傳入的集合類型,大小爲5,走else,進toMutableList
嘖嘖,看到這裏,你get到原理沒?
- 直接建立了一個新的ArrayList,把原列表做爲參數傳入初始化;
- 拿新列表的迭代器遍歷,移除的是舊列表中的元素,確定互不影響啊,和fail-safe(安全失敗)的玩法一模一樣。
參考文獻: