自從我上次發了《這道Java基礎題真的有坑!我求求你,認真思考後再回答。》這篇文章後。我經過這樣的一個行文結構:html
解析了小馬哥出的這道題,讓你們明白了這題的坑在哪裏,這題背後隱藏的知識點是什麼。git
可是我是萬萬沒想到啊,這篇文章竟然還有續集。由於有不少讀者給我留言,問我爲何?怎麼回事?啥狀況?程序員
問題片斷一:到底循環幾回?github
有不少讀者針對文章的下面的這個片斷:apache
來問了一些問題:爲何會循環三次?循環二次?循環一次?安全
源碼看的腦殼疼。那我以爲我須要"拯救"一下這個哥們了。併發
問題片斷二:爲何刪除第一個不出錯?框架
還有這個片斷,對於爲何刪除第一個元素不會拋出異常,也是一衆選手,不明就裏:ide
爲何?爲何沒有問題啊?oop
上面看着有點亂是否是呢?
那確定是你沒看過我這篇文章《這道Java基礎題真的有坑!我求求你,認真思考後再回答。》。不要緊,我先把問題提煉出來,而後有興趣你能夠再去看看這篇文章。
在描述問題以前,須要說明一下,爲了方便演示說明,我會去掉Java的foreach語法糖,直接替換爲編譯後的代碼,以下:
請坐穩扶好,下面的幾個問題有點繞。主要是看圖,先知道這幾個現象。以後我還會把問題再簡化一下。
問題一:如圖所示,爲何刪除第一個元素(公衆號)能夠正常執行,刪除第二個元素(why技術)就會拋出異常呢?
問題二:爲何當集合大小大於2時,刪除第一個元素(公衆號)也拋出了異常?
問題三:爲何刪除倒數第二個元素能夠正常執行?刪除倒數第二個元素之外的任意元素就會拋出異常?
問題四:爲何在刪除完成以後當即break,則能夠刪除任意元素呢?
問題五:如圖所示,爲何註釋掉判斷語句直接remove("why技術")不會報錯,而加上判斷語句就報錯了呢?
問題六:爲何判斷"why技術"並remove的時候循環三次?爲何註釋掉remove只循環兩次?爲何判斷"公衆號"並remove的時候只循環一次?
我再把問題彙總一下,你瞟一眼就行,不用細讀:
問題一:當集合大小等於2時,爲何刪除第一個元素(公衆號)能夠正常執行,刪除第二個元素(why技術)就會拋出異常呢?
問題二:爲何當集合大小大於2時,刪除第一個元素(公衆號)也拋出了異常?
問題三:爲何刪除倒數第二個元素能夠正常執行?刪除倒數第二個元素之外的任意元素就會拋出異常?
問題四:爲何在刪除完成以後當即break,則能夠刪除任意元素不會報錯呢?
問題五:爲何註釋掉判斷語句直接remove(why技術)不會報錯,而加上判斷語句就報錯了呢?
問題六:爲何判斷"why技術"並remove的時候循環三次?爲何註釋掉remove只循環兩次?爲何判斷"公衆號"並remove的時候只循環一次?
暈不暈?
不要暈。上面我只是爲了把各類狀況都執行一下,而後截圖出來,方便你們有個直觀的理解。其實,上面的這六個問題,我在看來就只有兩個問題:
1.當前循環會執行幾回?
2.爲何會拋出異常?
而這兩個問題中的第二個問題【爲何會拋出異常?】我已經在《這道Java基礎題真的有坑!我求求你,認真思考後再回答。》這篇文章中進行了十分詳盡的解答。因此,就不在這篇文章中討論了。
那麼,如今就只剩下一個問題了:當前循環會執行幾回?
本文會圍繞這個問題進行展開,當你明白這個問題後,上面的全部問題都迎刃而解了。
咱們就拿下面這個程序來進行分析:
我寫文章以前,在Debug模式下碰到了一些不是程序致使的意外bug(我懷疑是jdk或idea版本的問題),我最後會講一下,並且我以爲Debug模式也不太好對這個問題進行直觀的文字描述,須要截取大量圖片,這樣不太方便閱讀。因此爲了更好的解釋這個問題,更加方便你們閱讀,咱們先進行幾個"騷"操做,對程序進行一下改造。
正如上圖紅色粗線框起來的代碼所示。因爲這個循環體循環幾回是由while裏面的條件hasNext()方法,即【cursor!=size】這個條件決定的。
hasNext()方法是ArrayList中一個叫作Itr內部類中的一個方法。
若是咱們能把hasNext()方法修改爲這個樣子,加上幾行輸出,對於咱們的分析來講簡直完美,直觀,漂亮。(Java程序員確實是靠日誌活着。)
這裏咱們就不去編譯一套JDK而後修改源碼了,能夠投機取個巧,和我以前的文章中說的同樣,咱們自定義一個ArrayList。
改造點一:自定義ArrayList
咱們怎麼自定義ArrayList呢?
首先,咱們的需求是爲了演示問題方便,可是咱們的前提是得保證明驗對象的一致性,換句話說就是:咱們自定義的ArrayList須要和JDK的ArrayList的實現,如出一轍,只是換個名稱而已。
因此,咱們直接把JDK的ArrayList拷貝一份出來並修改一個名字便可。
直接拷貝一個ArrayList過來後你發現會有報錯的地方:
具體報錯的信息以下:
並不影響咱們此次的測試。因此直接註釋掉相關報錯的地方。爲了便於區分,咱們修更名稱爲WhyArrayList,並修改對應的代碼:
到這一步咱們自定義的ArrayList就算是改造完成了。只須要把他用起來便可,怎麼用,很簡單,替換原來的ArrayList便可,以下圖所示(若是不清晰,能夠點看看大圖哦):
可是我以爲輸出的日誌仍是不夠清晰,直觀。我想要直接輸出當前是第幾回循環,以下:
那咱們怎麼實現呢?這就是咱們的第二個改造點了。
改造點二:自定義Iterator
要實現上面的日誌輸出咱們很容易能想到第一個修改點,以下:
如今咱們的問題是怎麼把loopTime(循環次數)這個值傳進來。直接調用確定是不行的, Iterator並無這個方法。能夠看看提示:
那怎麼辦呢?
你想啊,Iterator是一個接口,既然它沒有這個方法,那咱們也就自定義一個WhyIterator繼承JDK的Iterator,而後在WhyIterator裏面定義咱們想要的接口便可:
而後咱們在WhyArrayList裏面只須要讓內部類Itr實現WhyIterator接口便可:
最後一步,調用起來,修改程序,並執行以下:
啊,這日誌,舒服了!
接下來,咱們進行喪心病狂的第三個改造點:
改造點三:一步一輸出
這一個改造點,我就不進行詳細說明了,授人以魚不如授人以漁,前面兩個改造點你若是會了,那你也能繼續改造,獲得下面的程序,並搞出一步一輸出日誌:
上面這圖,就是咱們最後須要分析的程序和日誌了。
若是你對於獲得上面的輸出仍是有點困難的話,你能夠在文末找到個人git地址,我把程序都上傳到了git上。
其實你想想,還用分析嗎?通過上面的三個"騷"操做後,真相已經擺在眼前了。
以這位讀者的問題舉例.
第一個問題:爲何判斷"why技術"並remove的時候循環三次?
你品一品這個輸出,這就是真相呀!爲何會循環三次,一目瞭然了啊!
【第1次循環】cursor=0,size=2,斷定結果:true
【第1次循環】var3.next方法被調用cursor進行加一操做
【第2次循環】cursor=1,size=2,斷定結果:true
【第2次循環】var3.next方法被調用cursor進行加一操做
【第2次循環】list.remove方法被調用size進行減一操做
【第3次循環】cursor=2,size=1,斷定結果:true
再回答另一個問題:爲何註釋掉remove只循環兩次?
你再品一品這個輸出:
第三個問題:爲何判斷"公衆號"並remove的時候只循環一次?
繼續品這個輸出:
致命一問,靈魂一擊
對於以前列舉的其餘問題,你有沒有發現其實有不少共同的地方,可是我故意擾亂了你的判斷,你仔細讀這幾個問題:
當集合大小等於2時,爲何刪除第一個元素(公衆號)能夠正常執行?
當集合大小大於2時,刪除第一個元素(公衆號)也拋出了異常?
爲何刪除倒數第二個元素能夠正常執行?
上面的三個問題實際上是在說一個問題,你發現了嗎?
當集合大小等於2時第一個元素(公衆號),是否是就是倒數第二個元素?!
恍然大悟有沒有?
再看一個示例:
下圖是上面示例的輸出:
敲黑板,數學推理來了:
在單線程的狀況下,只要你的ArrayList集合大小大於等於2(假設大小爲n,即size=n),你刪除倒數第二個元素的時候,cursor從0進行了n-1次的加一操做,size(即n)進行了一次減1的操做,因此n-1=n-1,即cursor=size。
由於判斷條件返回爲fales,雖然你的modCount變化了。可是不會進入下次循環,就不會觸發modCount和expectedModCount的檢查,也就不會拋出ConcurrentModifyException.
因此這個問題我也就回答了。
我在寫文章的過程當中,還有意外收穫。就是一個讀者提出的這個問題:爲何迭代器裏面的hasNext()裏面要用!=來判斷index和size之間的關係,而不是用<符號呢。
當時我並無留意到這個問題,我以爲就是均可以,可有可無。可是寫的時候我忽然想明白了,這可不是可有可無的事,這地方必須是 【!=】。
我給你看個表格:
在上面的程序中我把判斷條件改成了【cursor<size】,當執行到第三次循環,cursor=2,size=1時。用cursor<size返回的是false,則不會繼續循環,因此不會觸發fail-fast機制。若是用cursor!=size返回的是true,會繼續執行循環,因此會觸發檢查modCount的操做,觸發fail-fast機制。
正如我截圖中說的:這裏用【!=】判斷,是符合它的語境的。用迭代器循環的時候,循環結束的條件就是循環到最後一個元素就中止循環。可是這一條件的前提是在我循環的過程當中,集合大小是固定的。若是集合大小發生了變化,那就會觸發fail-fast機制。
說到這個問題,我真的以爲我被智子封鎖了,我開始理解那些科學家爲何要自殺了。若是你讀過《三體》,你知道我在說什麼。
不管是用咱們自定義的WhyArrayList仍是JDK的ArrayList結果都是同樣的,爲告終果的直觀,我用WhyArrayList給你演示一下:
第一步是沒有問題的:
可是當進入第一次循環,cursor=1,return以前又變成了2。
因此程序在Debug模式下的輸出變成了這樣:
個人Idea版本是:IntelliJ IDEA 2019.2.4 (Ultimate Edition)
個人JDK版本信息以下:
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_212-b03)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.212-b03, mixed mode)
若是你也碰到過,你知道是怎麼狀況,請你告訴我到底是怎麼回事,是否是計劃的一部分。
本文前傳
答應我,若是你不知道這個知識點,想徹底掌握的話,必定要去讀一讀本文的前傳《這道Java基礎題真的有坑!我求求你,認真思考後再回答。》。兩篇文章合計一塊兒食用,味道更佳。
本文代碼
本文的源碼我已經上傳到git上了,git地址以下:
git clone git@github.com:thisiswanghy/WhyArrayList.git
fail-fast機制和fail-safe機制
文中屢次提到了"fail-fast"機制(快速失敗),與其對應的還有"fail-safe"機制(失敗安全)。
這種機制是一種思想,它不只僅是體如今Java的集合中。在咱們經常使用的rpc框架Dubbo中,在集羣容錯時也有相關的實現。
Dubbo 主要提供了這樣幾種容錯方式:
Failover Cluster - 失敗自動切換
Failfast Cluster - 快速失敗
Failsafe Cluster - 失敗安全
Failback Cluster - 失敗自動恢復
Forking Cluster - 並行調用多個服務提供者
若是對這兩種機制感興趣的朋友能夠查閱相關資料,進行了解。若是想要了解Dubbo的集羣容錯機制,能夠看官方文檔,地址以下:
http://dubbo.apache.org/zh-cn/docs/source_code_guide/cluster.html
Java語法糖
文中說到foreach循環的時候提到了Java的語法糖。若是對這一塊有興趣的讀者,能夠在網上查閱相關資料,也能夠看看《深刻理解Java虛擬機》的第10.3節,有專門的介紹。
書中說到:
總而言之,語法糖能夠看作是編譯器實現的一些「小把戲」,這些「小把戲」可能會使得效率「大提高」,但咱們也應該去了解這些「小把戲」背後的真實世界,那樣才能利用好它們,而不是被它們所迷惑。
阿里Java開發手冊
阿里Java開發手冊中也有對該問題的描述,強制要求:
不要在foreach循環裏面進行元素的remove/add操做。remove元素請使用Iterator方式,若是併發操做,須要對Iterator對象加鎖。
阿里的孤盡大佬做爲主要做者寫的這本《阿里Java開發手冊》,能夠說是嘔心瀝血推出的業界權威,很是值得閱讀。讀完此書,你不只可以得到不少乾貨,甚至你還能讀出一點技術情懷在裏面。
對於技術情懷,孤盡大佬是這樣的說的:
熱愛、思考、卓越。熱愛是一種源動力,而思考是一個過程,而卓越是一個結果。若是給這三個詞加一個定語,使技術情懷更加立體、清晰地被解讀,那就是奉獻式的熱愛,主動式的思考,極致式的卓越。
關注公衆號並回復關鍵字【Java】。便可得到此書的電子版。
若是你以前對於這個知識點掌握的不牢固,讀完這篇文章以後你會知道有這麼一個知識點,可是僅僅是知道,不是一個十分具化的印象。只有你實際的操做一下以後,才能算是掌握了,源碼會刻在你的潛意識裏面。久久不會忘記。這部分如今對我來講,我輸出了共計1萬3千多字的文章,在個人腦海中固若金湯。
因此我我的建議,最好再去實際操做一下吧。git地址我前面給你了。
再推銷一下我公衆號:對於寫文章,其實想到寫什麼內容並不難,難的是你對內容的把控。關於技術性的語言,我是反覆推敲,查閱大量文章來進行證僞,總之慎言慎言再慎言,畢竟作技術,我認爲是一件很是嚴謹的事情,我經常想象本身就是在故宮修文物的工匠,在工匠精神的認知上,目前我可能和他們還差的有點遠,可是我時常以工匠精神要求本身。就像我以前表達的:對於技術文章(由於我偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評),我儘可能保證周推,全力保證質量。堅持輸出原創。
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
以上。
謝謝您的閱讀,感謝您的關注。