1. 程式人生 > >【原創】這道Java基礎題真的有坑!我也沒想到還有續集。

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

前情回顧

自從我上次發了《這道Java基礎題真的有坑!我求求你,認真思考後再回答。》這篇文章後。我通過這樣的一個行文結構:

解析了小馬哥出的這道題,讓大家明白了這題的坑在哪裡,這題背後隱藏的知識點是什麼。

但是我是萬萬沒想到啊,這篇文章居然還有續集。因為有很多讀者給我留言,問我為什麼?怎麼回事?啥情況?

問題片段一:到底迴圈幾次?

有很多讀者針對文章的下面的這個片段:

來問了一些問題:為什麼會迴圈三次?迴圈二次?迴圈一次?

原始碼看的腦袋疼。那我覺得我需要"拯救"一下這個哥們了。

問題片段二:為什麼刪除第一個不出錯?

還有這個片段,對於為什麼刪除第一個元素不會丟擲異常,也是一眾選手,不明就裡:

為什麼?為什麼沒有問題啊?

提煉問題

上面看著有點亂是不是呢?

那肯定是你沒看過我這篇文章《這道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 [email protected]: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地址我前面給你了。

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

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