1. 程式人生 > >這道Java基礎題真的有坑!我求求你,認真思考後再回答。

這道Java基礎題真的有坑!我求求你,認真思考後再回答。

本文目錄

一.題是什麼題?

二.阿里Java開發規範。

    2.1 正例程式碼。

    2.2 反例程式碼。

三.層層揭祕,為什麼發生異常了呢?

    3.1 第一層:異常資訊解讀。

    3.2 第二層:丟擲異常的條件解讀。

    3.3 第三層:什麼是modCount?它是幹啥的?什麼時候發生變化?

    3.4 第四層:什麼是expectedModCount?它是幹啥的?什麼時候發生變化?

    3.5 第五層:組裝線索,直達真相。

四.這題的坑在哪?

    4.1 回頭再看。

    4.2 還有一個騷操作。

五.執行緒安全版的ArrayList。

六.總結一下。

七.回答另外一個面試題。

八.擴充套件閱讀。

     7.1 fail-fast機制和safe-fast機制。

     7.2 Java語法糖。

     7.3 阿里Java開發手冊。

一.題是什麼題?

我第一次遇到這個題的時候,是在一個微信群裡,阿里著名的"Java勸退師"小馬哥丟擲了這樣的一個問題:

然後大家紛紛給出了自己的見解(注:刪除了部分聊天記錄):

後面在另外的群裡聊天的時候(注:刪除了部分聊天記錄),我也丟擲了這樣的問題:

總結一下圖片中的各種回答:

1.什麼也不會發生,remove之後,list中的資料會被清空。

2.remove的方法呼叫錯誤,入參應該是index(陣列下標)。

3.併發操作的時候會出現異常。

4.會發生ConcurrentModifyException。

你的答案又是什麼呢?

在這裡,我先不說正確的答案是什麼,也先不評價這些回答是對是錯,我們一起去探索真相,尋找答案。

二.阿里Java開發規範

有人看到題的第一眼(沒有認真讀題),就想起了阿里java開發手冊(先入為主),裡面是這樣說的:

正是因為大多數人都知道並且讀過這個規範(畢竟是業界權威)。所以呼聲最高的答案是【會發生ConcurrentModifyException】。因為他們知道阿里java開發手冊裡面是強制要求:

不要在foreach迴圈裡面進行元素的remove/add操作。remove元素請使用Iterator方式,如果併發操作,需要對Iterator物件加鎖。

但是不能因為他是權威,我們就全盤接受吧?

2.1 正例程式碼

所以我們眼見為實,先把手冊裡面提到的【正例程式碼】跑一下,如下:

細心的讀者可能發現了:咦,這個程式碼的22行為啥顏色不一樣呢?

我幫你看看。

替換之後的程式碼是這樣的:

從上面我們可以得到一個結論.......

等等,到這一步你就想得到結論了?你不對【一行程式碼為什麼就替換了七行程式碼】好奇嗎?

看到真相的時候,有時候再往前一步就是本質了。

原始碼之下無祕密,我再送你一張圖,JDK1.8中Collection.removeIf的原始碼:

好了,已經到原始碼級別了,從這裡我們驗證了,阿里java開發手冊裡面的正例是對的,而且我還想給他加上一句:

如果你的JDK版本是1.8以上,沒有併發訪問的情況下,可以使用Collection.removeIf(Predicate<? super E> filter)方法。使程式碼更加優雅。

2.2 反例程式碼

接下來我們看看【反例程式碼】的執行結果:

從執行結果來看,和我們預期的結果是一致。看著沒有問題呀?

但是你別忘了,下面還有一句話啊:

我們執行試一試:

什麼情況?真的是"出乎意料"啊!

把刪除元素的條件從【公眾號】修改為【why技術】就發生了異常:

java.util.ConcurrentModificationException

三.層層揭祕,為什麼發生了異常呢?

我們現在明白為什麼阿里強制要求不要在foreach迴圈裡面進行元素的remove/add操作,因為會發生異常了。

但是開發手冊裡面並沒有告訴你,為什麼會發生異常。需要我們自己層層深入,積極探索。

3.1 第一層:異常資訊解讀

所以這一小節我們就一起探索,為什麼會發生異常。我們再解析一下程式的執行結果,如下:

正如上圖裡面異常資訊的體現,異常是在程式碼的第21行觸發的。而程式碼的第21行,是一個foreach迴圈。foreach迴圈是Java的語法糖,我們可以從編譯後的class檔案中看出,如下圖所示:

請注意圖中的第26行程式碼:

list.remove(item)  (這句話很關鍵!!!)

很關鍵,很重要,後面會講到。

這也解釋了,異常資訊裡面的這一個問題:

好了,到這一步,我們把異常資訊都解讀完畢了。

3.2 第二層:丟擲異常的條件解讀

我再看看真實丟擲異常的那一個方法:

很簡單,很清晰的四行程式碼。丟擲異常的條件是:

modCount !=expectedModCount

所以,我們需要解開的下兩層面紗就是下面兩大點:

第一:什麼是modCount?它是幹啥的?什麼時候發生變化?

第二:什麼是expectedModCount?它是幹啥的?什麼時候發生變化?

3.3 第三層:什麼是modCount?它是幹啥的?什麼時候發生變化?

先來第一個:什麼是modCount?

modCount上的註釋很長,我只截取了最後一段。在這一段中,提到了兩個關鍵點。

1.modCount這個欄位位於java.util.AbstractList抽象類中。

2.modCount的註釋中提到了"fail-fast"機制。

3.如果子類希望提供"fail-fast"機制,需要在add(int,E)方法和remove(int)方法中對這個欄位進行處理。

4.從第三點我們知道了,在提供了"fail-fast"機制的容器中(比如ArrayList),除了文中示例的remove(Obj)方法會導致ConcurrentModificationException異常,add及其相關方法也會導致異常。

知道了什麼是modCount。那modCount是幹啥的呢?

在提供了"fail-fast"機制的集合中,modCount的作用是記錄了該集合在使用過程中被修改的次數。

證據就在原始碼裡面,如下:

這是java.util.ArrayList#add(int, E)方法的原始碼截圖:

這是java.util.ArrayList#remove(int)方法的原始碼截圖:

注:這裡不討論手動設定為null是否對GC有幫助,我個人認為,在這裡有這一行程式碼並沒有壞處。在實際開發過程中,一般不需要考慮到這點。

同時,上面的原始碼截圖也回答了這一層的最後一個問題:它什麼時候被修改?

拿ArrayList來說,當呼叫add相關和remove相關方法時,會觸發modCount++操作,從而被修改。

好了,通過上面的分析,我們知道了什麼是modCount和modCount是幹啥的。準備進入第四層。

3.4 第四層:什麼是expectedModCount?它是幹啥的?什麼時候發生變化?

接下來:什麼是expectedModCount?

expectedModCount是ArrayList中一個名叫Itr內部類的成員變數。

第二問:expectedModCount它是幹啥的:

它代表的含義是在這個迭代器中,預期的修改次數

第三問:expectedModCount什麼時候發生變化?

情況一:從上圖中也可以看出當Itr初始化的時候,會對expectedModCount欄位賦初始值,其值等於modCount。

情況二:如下圖所示,呼叫Itr的remove方法後會再次把modCount的值賦給expectedModCount。

換句話說就是:呼叫迭代器的remove會維護expectedModCount=modCount。(這句話很關鍵!!!)

好了分析到了這裡,我們知道了下面這個六連擊:

1.什麼是modCount?

2.modCount是幹啥的?

3.modCount什麼時候發生變化?

4.什麼是expectedModCount?

5.expectedModCount是幹啥的?

6.expectedModCount什麼時候發生變化?

3.5 第五層:組裝線索,直達真相

為什麼發生了異常呢?

如果說前四層是線索的話,真相其實已經隱藏線上索裡面了。我帶你梳理一下:

【第一層:異常資訊解讀】中說到:

【第二層:丟擲異常的條件解讀】中說到:

【第三層:什麼是modCount?它是幹啥的?什麼時候發生變化?】中說到:

【第四層:什麼是expectedModCount?它是幹啥的?什麼時候發生變化?】中說到:

為什麼發生了異常呢?我想你大概已經有了一個答案了,我再去Debug一下,為了方便演示,我們去掉語法糖,程式修改如下:

並確認一下這個迴圈體會執行三次,如下:

第一次迴圈取出的【公眾號】,不滿足條件if("why技術".equals(item)),不會觸發list.remove(Obj)方法。

第二次迴圈

如圖所示,第二次迴圈取到了“why技術”。滿足條件if("why技術".equals(item)),會觸發list.remove(Obj)方法,如下所示:

第三次迴圈

總結一下在foreach迴圈裡面進行元素的remove/add操作丟擲異常的真相:

因為foreach迴圈是Java的語法糖,經過編譯後還原成了迭代器。

但是從經過編譯後的程式碼的第26行可以看出,remove方法的調方是list,而不是迭代器。

經過前面的原始碼分析我們知道,由於ArrayList的"fail-fast"機制,呼叫remove方法會觸發【modCount++】操作,對expectedModCount沒有任何操作。只有呼叫迭代器的remove方法,才會維護expectedModCount=modCount。

所以呼叫了list的remove方法後,再呼叫Itr的next方法時,導致了expectedModCount!=modCount,丟擲異常。

四.這題的坑在哪裡?

前面講了阿里開發手冊。講了在foreach迴圈裡面進行元素的remove/add為什麼會發生異常。有了這些鋪墊之後。

4.1 回頭再看

我們再回過頭來看小馬哥出的這個題:

我靠,這乍一看,foreach迴圈裡面呼叫list.remove(obj)。我們剛剛分析過,會丟擲ConcurrentModificationException異常。

你要這樣答,你就進了小馬哥的坑了。

這個題的坑在這三個點裡面。小馬哥並沒有說這個list是ArrayList吧?如果你沒有認真審題,先入為主的默認了這個list就是ArrayList。第一步就錯了。

這是真正的高手,借力打力。借阿里開發手冊的力,讓你第一步就走錯。

請看下面這張圖:

當使用CopyOnWriteArrayList的時候,程式正常執行。

4.2 還有一個騷操作

既然我們知道為什麼會丟擲異常,也知道怎麼不丟擲異常,List本來就是一個介面,那我們是不是可以實現這個介面,弄一個自定義的List呢?

比如下面的這個WhyTechnologyList,就是我自己的List,狸貓換太子,這操作,夠"騷"啊。

只有掌握了原理,我們想怎麼玩就怎麼玩。

五.執行緒安全版的ArrayList

CopyOnWriteArrayList是什麼?我們看一下原始碼註釋上面是怎麼說的:

相對於ArrayList而言,CopyOnWriteArrayList集合是執行緒安全的容器。在遍歷的時候,由於它操作是陣列的"快照","快照"不會發生變化。所以它不需要額外加鎖,也不會丟擲ConcurrentModificationException異常。

我們主要看一下,示例程式中用到的三個方法,add(E e)、next()、remove(Obj)

先看add(E e)方法:

我們看一下它的next()方法:

再看一下它的remove(Obj)方法:

next、remove都是操作的快照,並沒有看到ArrayList裡面的modCount和expectedModCount。所以它沒有丟擲ConcurrentModificationException

之前看小馬哥說的這句話的時候還不太明白集合和一致性之間的關係(老問題,還是先入為主,一說到一致性首先想到的是快取和資料庫之間的一致性)。

但是當我閱讀原始碼,從add方法可以看出CopyOnWriteArrayList並不保證資料的實時一致性。只能保證最終一致性。

同時我們從原始碼中可以看出CopyOnWriteArrayList增刪改資料的時候需要搞一個"快照",這一點是比較耗記憶體的,使用過程中需要注意。

六.總結一下

我們再回到最開始的地方,看看大家的回答:

1.什麼也不會發生,remove之後,list中的資料會被清空。

2.remove的方法呼叫錯誤,入參應該是index(陣列下標)。

3.併發操作的時候會出現異常。

4.會發生ConcurrentModifyException。

現在,你知道這些回答的問題在哪裡了吧?這一部分的總結也很簡單,上一個對比圖就好了,如果看不清楚,你可以點開看大圖:

ArrayList CopyOnWriteArrayList

七.回答另外一個面試題

現在面試官經常問的一個問題,你讀過原始碼嗎?

咦,巧了。你看了這篇文章,就相當於了讀了ArrayList和CopyOnWriteArrayList的部分原始碼。

那你就可以這樣回答啦:我之前看阿里Java開發手冊的時候看到一條規則是

不要在foreach迴圈裡面進行元素的remove/add操作。remove元素請使用Iterator方式,如果併發操作,需要對Iterator物件加鎖。

我對這條規則很感興趣,所以我對其進行了深入的研究,閱讀了

ArrayList和CopyOnWriteArrayList的部分原始碼。

如果碰巧面試官也讀過這塊原始碼,這個問題,你們可以相談甚歡。

如果面試官沒有讀過這塊原始碼,你可以給他講的明明白白。

當然,還有一個前提是:我希望你讀完這篇文章後,如果是第一次知道這個知識點,那你可以自己實際操作一下。

看懂了是一回事,自己再實際操作一下,是另外一回事。

八.擴充套件閱讀

8.1 fail-fast和safe-fast機制

文中多次提到了"fail-fast"機制(快速失敗),與其對應的還有"safe-fast"機制(失敗安全)。

這種機制是一種思想,它不僅僅是體現在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

8.2 Java語法糖

文中說到foreach迴圈的時候提到了Java的語法糖。如果對這一塊有興趣的讀者,可以在網上查閱相關資料,也可以看看《深入理解Java虛擬機器》的第10.3節,有專門的介紹。

書中說到:

總而言之,語法糖可以看做是編譯器實現的一些“小把戲”,這些“小把戲”可能會使得效率“大提升”,但我們也應該去了解這些“小把戲”背後的真實世界,那樣才能利用好它們,而不是被它們所迷惑。

8.3 阿里Java開發手冊

阿里的孤盡大佬作為主要作者寫的這本《阿里Java開發手冊》,可以說是嘔心瀝血推出的業界權威,非常值得閱讀。讀完此書,你不僅能夠獲得很多幹貨,甚至你還能讀出一點技術情懷在裡面。

對於技術情懷,孤盡大佬是這樣的說的:

熱愛、思考、卓越。熱愛是一種源動力,而思考是一個過程,而卓越是一個結果。如果給這三個詞加一個定語,使技術情懷更加立體、清晰地被解讀,那就是奉獻式的熱愛,主動式的思考,極致式的卓越。

關注公眾號並回復關鍵字【Java】。即可獲得此書的電子版。

九.最後說一點

這篇文章寫之前我一直在糾結,因為感覺這個知識點其實我已經掌握了,那我還有寫的必要嗎?我在寫的這個過程中還能收穫一些東西嗎?

但是在寫的過程中,我翻閱了大量的原始碼,雖然之前已經看過,但是沒有這樣一行一行仔細的去分析。之前只是一個大概的模糊的影像,現在具象化清晰了起來,在這個過程中,我還是學到了很多很多。

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

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

如果你覺得文章還不錯,你的點贊、留言、轉發、分享、讚賞就是對我最大的鼓勵

以上。

謝謝您的閱讀,感謝您的關注。公眾號會是文章首發平臺,關注可以第一時間看到原創文章哦。