【原創】這道Java基礎題真的有坑!我也沒想到還有續集。

前情回顧

自從我上次發了《這道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機制。

智子封鎖:Debug下的問題

說到這個問題,我真的以爲我被智子封鎖了,我開始理解那些科學家爲何要自殺了。若是你讀過《三體》,你知道我在說什麼。

不管是用咱們自定義的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地址我前面給你了。

再推銷一下我公衆號:對於寫文章,其實想到寫什麼內容並不難,難的是你對內容的把控。關於技術性的語言,我是反覆推敲,查閱大量文章來進行證僞,總之慎言慎言再慎言,畢竟作技術,我認爲是一件很是嚴謹的事情,我經常想象本身就是在故宮修文物的工匠,在工匠精神的認知上,目前我可能和他們還差的有點遠,可是我時常以工匠精神要求本身。就像我以前表達的:對於技術文章(由於我偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評),我儘可能保證周推,全力保證質量。堅持輸出原創。

才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
以上。
謝謝您的閱讀,感謝您的關注。

相關文章
相關標籤/搜索