這道面試題我真不知道面試官想要的回答是什么

這是why技術(shù)的第15篇原創(chuàng)文章

面試是一個很奇怪的過程,都是擰螺絲的。但是問的都是如何造火箭,一個敢問,一個敢答。

面試不可怕,可怕的是你get不到面試官的點。

更可怕的是,你覺得你知道答案,但不是面試官想要的。

最可怕的是,面試官也不知道這題的答案是什么。

送分題?送命題?

前段時間有個小伙伴在一個群里分享了一道親身經(jīng)歷的面試題,這題乍一看好像張口就能答,但是仔細一想,面試官是想要這樣的回答嗎?具體可以看截圖。

file

可以想象一下那個略顯尷尬的畫面:

面試官:請問ConcurrentHashMap中的key為什么不能為null?

面試者:因為源碼里面就是這樣寫的,判斷為空,拋出異常。

面試官:沒了?

面試者:沒了。

我前思后想,對于這個問題我是真的不知道面試官想要什么樣的答案。就算我寫完這篇文章之后,我知道了前因后果,我還是不清楚怎么回答他的這個問題。因為我get不到他的點在哪里。

具體怎么回事,看完本文之后,你就知道了。

我提煉并升華一下這個面試題,請問:

ConcurrentHashMap為什么不能存值為null的value?

ConcurrentHashMap為什么不能放值為null的key?

SHOW ME THE CODE

我們先看一下當(dāng)ConcurrentHashMap的key和value分別都為null的時候,程序的執(zhí)行結(jié)果是什么:

file

可以看到,這里拋出了空指針異常,因為ConcurrentHashMap里面的key和value是都不能為null的。

其對應(yīng)的源碼部分如下(JDK 1.8):

file

有的時候,你看到源碼說明你看的很深入了;

有的時候,你看到源碼了,只是看到了表象。

比如這個地方,源碼為什么這樣寫?或者換個問法,作者這樣寫是基于什么考慮的?

if (key == null || value == null) throw new NullPointerException();

要知道作者這樣寫的出發(fā)點是什么,最權(quán)威的回答就是作者自己的回答。而ConcureentHashMap就是巨佬Doug Lea老爺子寫的。

file

Doug Lea是誰?java.util.concurrent包你知道吧?他寫的 。

俗話說得好:編程不識Doug Lea,寫盡Java也枉然。

file

啊,為什么老爺子這么強,還有這么多頭發(fā)。

file

知道他是誰了,接下來就好辦了。因為早在2006年就有人針對ConcurrentHashMap的key和value為什么不能為null的問題寫過郵件咨詢過,而他老爺子親自回答了這個問題。

本文在翻譯四封相關(guān)郵件的過程中,結(jié)合老爺子的郵件,加上自己的理解來回答這個問題。

說明:本人英文水平有限,翻譯出來的文章大家看的時候多多包涵。同時我也附上原文和郵件地址,大家可以訪問。

第一封:Tutika求助

郵件地址:http://cs.oswego.edu/pipermail/concurrency-interest/2006-May/002482.html

2006年5月12日早上06點01分45秒,一位名叫Tutika的網(wǎng)友發(fā)出了"求助"郵件:

file

郵件內(nèi)容如下:

file

全文翻譯過來,大概就是:

大家好,我想把我一個多線程的項目里面一些HashMap用ConcurrentHashMap替換掉。在HashMap里面我可以放key或者value為null的數(shù)據(jù),沒有任何毛病。但是ConcurrentHashMap的key和value都不允許為null。

我想知道針對這一問題,有沒有比較好的解決方式。需要說明一下的是,在我的應(yīng)用程序中,對于值為null的value和key是非常難以判斷的。

我的解決方案是想包裝一下ConcurrentHashMap,當(dāng)插入null值的時候用其他的對象來代替,取出該對象時再轉(zhuǎn)換為null。但是這個解決方案的問題是在比如keySet(),values()這樣的批量操作的方法中,進行對應(yīng)的轉(zhuǎn)換是非常困難的。

如果有人對于這個問題有解決思路,請告訴我。這將對我非常有用。

翻譯結(jié)束。

這里我想插個題外話,關(guān)于提問的藝術(shù),我覺得Tutika同學(xué)的提問方式就很標準。在什么場景下遇到了什么問題,自己嘗試的解決方案是什么,請問有沒有更好的解決方案?

好好看看下面的圖,別一上來就是:有人嗎?在嗎?

file

第二封:熱心網(wǎng)友

郵件地址:http://cs.oswego.edu/pipermail/concurrency-interest/2006-May/002484.html

Tutika發(fā)出"求救"郵件后的1小時20分18秒,就有熱心網(wǎng)友Holger回復(fù)了他的問題,

file

原版全文如下:

file
file
file

我再來翻譯一下:

Tutika:我想把我一個多線程的項目里面的一些HashMap用ConcurrentHashMap替換掉。

Holger:在這樣做之前,你必須了解到雖然這樣的解決方案看起來好像可以解決你的問題,但是它隨之可能給你帶來意想不到的結(jié)果。某些隱藏很深的原因,他們可能會通過諸如ConcurrentModificationException的形式表現(xiàn)出來。最好是解決并發(fā)訪問的問題,而不是用ConcurrentHashMap來掩蓋問題,因為在這個明顯的問題被“修復(fù)”之后,你很可能會遇到其他的由于并發(fā)帶來的bug。

Tutika:在hashMap里面我可以放key或者value為null的數(shù)據(jù),沒有任何毛病。

Holger認為HashMap里面可以存放null是Java Map類的一個嚴重錯誤。

Tutika:但是ConcurrentHashMap的key和value都不允許為null。我想知道針對這一問題,有沒有人有比較好的方式去解決。

Holger的建議是在調(diào)用方加入檢查key和value都不能為空的邏輯。如果你們有單元測試,請在測試中包含對這個邏輯的測試。

Tutika:在我的應(yīng)用程序中,對于值為null的value和key是非常難以判斷的。

Holger:這就是使用允許存放null的HashMap所要付出的代價。

Tutika:我想包裝一下ConcurrentHashMap,當(dāng)插入null值的時候用其他的對象來代替,再取出該對象時再轉(zhuǎn)換為null。但是這個解決方案的問題是在比如keySet(),values()這樣的批量操作的方法中,進行值轉(zhuǎn)換是非常困難的。

Holger:即使這樣,你仍然會遇到這樣的問題:首先你需要找到現(xiàn)有Map的構(gòu)造函數(shù)的所有調(diào)用方并修復(fù)它們。而且這也是不可能的,比如你有可能是從其他地方獲取到這個Map的。

Tutika:如果有人對于這個問題有解決思路,請告訴我。這將對我非常有用。

Holger給出了下面兩個選擇:

1.首先得接受你的程序是有并發(fā)問題的,你得找到問題的原因,而不是試圖用ConcurrentHashMap來掩蓋問題。這只是一個表明有其他事情不對勁的信號。意味著你得對整個應(yīng)用程序或受影響的子系統(tǒng)(如果有的話)進行充分的并發(fā)分析,也意味著你必須嚴格的審視你應(yīng)用程序里面有并發(fā)訪問的地方。找到之后你可以再使用Collections.synchronizedMap()或者ConcurrentHashMap來解決。

2.用AOP技術(shù)來解決你的問題。我已經(jīng)附加了一個簡單的AspectJ MapCheck切面,您可以將其編織到你的應(yīng)用程序中。在我的示例中是拋出IllegalArgumentExceptions,當(dāng)然,你可以根據(jù)你的場景修改為跳過這次put操作,或者放默認值。你需要非常認真的評估這是否適合你的場景,因為當(dāng)調(diào)用者錯誤地傳了一個空鍵,你最終可能會用默認鍵替換值。我給出的切面是要盡早暴露空鍵/值問題。在你的業(yè)務(wù)場景下,也許跳過這個操作也是可以接受的。

file

總之,解決你的問題沒有捷徑。

翻譯結(jié)束。

我來總結(jié)一下Holger這個哥們說了什么:

1.你這個程序是有并發(fā)問題的,僅僅引入ConcurrentHashMap是治標不治本的方法。

2.在HashMap里面允許放值為null的鍵/值,就是一個錯誤的設(shè)計。

3.你給出的解決方案是不好的。

4.我給你建議就是你得找到有并發(fā)問題,但是自己沒有控制好的部分。找到問題的根源。

5.或者你用AOP技術(shù)來解決你的問題,雖然我不推薦,但是我還是給你寫個示例,我這里是拋出異常,你可以根據(jù)你的業(yè)務(wù)場景具體情況具體分析。

6.你這個問題不太好搞,我只能幫到這里了。

第三封:巨佬現(xiàn)身

郵件地址:http://cs.oswego.edu/pipermail/concurrency-interest/2006-May/002485.html

在Tutika發(fā)出求救郵件后的2小時又47秒后,

file

ConcurrentHashMap的作者,Doug老爺子親自回答了這個問題。這是這個問題的高光時刻,也是本文的高光時刻,全文如下,

file
file

翻譯一下:

Tutika:我想把我一個多線程的項目里面的一些HashMap用ConcurrentHashMap替換掉。在hashMap里面我可以放key或者value為null的數(shù)據(jù),沒有任何毛病。但是ConcurrentHashMap的key和value都不允許為null。
對于熱心網(wǎng)友Holger的郵件,Doug說:你可以試著接受Holger的建議,雖然他都沒有說到點子上...

對于Tutika提出的問題,Doug給出的回答是:在ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps)這些考慮并發(fā)安全的容器中不允許null值的出現(xiàn)的主要原因是他可能會在并發(fā)的情況下帶來難以容忍的二義性。而在非并發(fā)安全的容器中,這樣的問題剛好是可以解決的。在map容器里面,調(diào)用map.get(key)方法得到的值是null,那你無法判斷這個key是在map里面沒有映射過,還是這個key在map里面根本就不存在。這種情況下,在非并發(fā)安全的map中,你可以通過map.contains(key)的方法來判斷。但是在考慮并發(fā)安全的map中,在兩次調(diào)用的過程中,這個值是有可能被改變的。

接下來Doug說了個題外話:我個人認為,在Maps或者Sets集合中允許null值的存在,就是公開邀請錯誤進入你的程序。而這些錯誤,只有在發(fā)生錯誤的情況下才能被發(fā)現(xiàn)。(我覺得在非并發(fā)安全的Maps和Sets中是否應(yīng)該允許null的存在的這個問題,是關(guān)于集合的少數(shù)幾個設(shè)計問題之一,這也Josh Bloch和我長期以來一直在爭執(zhí)的話題。)

Tutika:在我的整個應(yīng)用程序中,對于值為null的value和key是非常難以判斷的。

Doug給出的建議是:可以試一試在某個地方聲明static final Object NULL=new Object(),然后用NULL替換掉所有用null的地方。

翻譯結(jié)束。

我再來解析一下Doug老爺子說了什么。

首先他對于Holger的建議進行了調(diào)侃:可以使用他的建議,但是他沒有說到點子上。

說主要原因時,Doug用了反證法,先假定ConcurrentHashMap也可以存放value為null的值。那不管是HashMap還是ConcurrentHashMap調(diào)用map.get(key)的時候,如果返回了null,那么這個null,都有兩重含義:

**1.這個key從來沒有在map中映射過。

**2.這個key的value在設(shè)置的時候,就是null。

他說在非線程安全的map集合(HashMap)中可以使用map.contains(key)方法來判斷,而ConcurrentHashMap卻不可以。

我用程序來表示一下他的具體意思。

首先,先說HashMap,因為HashMap是線程不安全的(補充一句廢話:如果只讀不寫,HashMap也是線程安全的),所以,我們對于HashMap的正確使用場景是在單線程下使用。如下:

file

輸出的結(jié)果為:

file

在上面的實例中,由于是單線程,當(dāng)我們得到的value是null的時候,我可以用hashMap.containsKey(key)方法來區(qū)分上面說的兩重含義。

按照上面的程序,第一次判斷可以知道這個key從來沒有在map中映射過。第二次判斷可以知道這個key的value在設(shè)置的時候,就是null。

所以當(dāng)map.get(key)返回的值是null,在HashMap中雖然存在二義性,但是結(jié)合containsKey方法可以避免二義性。

但是如果是ConcurrentHashMap呢?它的使用場景是多線程的情況下。我們還是用反證法來推理,假設(shè)concurrentHashMap允許存放值為null的value。

這時有A、B兩個線程。

線程A調(diào)用concurrentHashMap.get(key)方法,返回為null,我們還是不知道這個null是沒有映射的null還是存的值就是null。

我們假設(shè)此時返回為null的真實情況就是因為這個key沒有在map里面映射過。那么我們可以用concurrentHashMap.containsKey(key)來驗證我們的假設(shè)是否成立,我們期望的結(jié)果是返回false。

但是在我們調(diào)用concurrentHashMap.get(key)方法之后,containsKey方法之前,有一個線程B執(zhí)行了concurrentHashMap.put(key,null)的操作。那么我們調(diào)用containsKey方法返回的就是true了。這就與我們的假設(shè)的真實情況不符合了。

這就是Doug說的在兩次調(diào)用的過程中值是可能變化的(the map might have changed between calls.)。這就是Doug所要表達的二義性。

以上也是Doug對這個面試題(為什么ConcurrentHashMap中的value不允許為null)的回答。

但是對于為什么key不能為null沒有給出直接回答。

在郵件的最后,Doug對Tutika遇到的問題給出了自己的建議:可以定義一個名稱為NULL的全局的Object。當(dāng)需要用null值的時候,用這個NULL來代替,以假亂真。

同時,在郵件里他還表達了個人的觀點:他認為不管容器是否考慮了線程安全問題,都不應(yīng)該允許null值的出現(xiàn)。他覺得在現(xiàn)有的某些集合里面允許了null值的出現(xiàn),是集合的設(shè)計問題。他也一直在和Josh Bloch討論這個事情。

那么這個Josh Bloch是何許人也?

file
file
file

詞條里面說到一本書《Effective Java》,我個人認為是Java屆的一本圣經(jīng)。如果你不知道,我勸你讀一讀,記得放在枕頭邊上。同時他還是HashMap的作者之一,所以他對于HashMap是很有發(fā)言權(quán)的。

而且,啊,為什么他這么強,也有這么多頭發(fā)。

第四封郵件:Josh回應(yīng)

郵件地址:http://cs.oswego.edu/pipermail/concurrency-interest/2006-May/002486.html

在Doug在郵件里面cue到他的4小時19分34秒后,Josh也發(fā)出了一份郵件:

file

郵件內(nèi)容如下:

file

Josh的郵件里說:Doug,這些年來我已經(jīng)站在你的立場了。Maps集合中允許值為null的key和在Sets中允許null元素可能真的是一個錯誤。但是對于是否應(yīng)該允許值為null的value存在,這點我還在思考。

另外,Josh想說的是,Doug比他更加討厭null。但是這些年來,他也發(fā)現(xiàn)null是一個非常令人頭疼的問題。

我來解讀一下Josh想要表達的觀點:

1.Doug你錯怪我了,你不應(yīng)該用爭執(zhí)來形容我們之間的問題,對于你的觀點我已經(jīng)接受一半了,另外一半我還在思考。

2.Doug你是對的,null真的是一個讓人頭疼的存在。

也許,從Josh這里,我能獲取到為什么concurrentHashMap的key不能為null。因為Doug討厭null值,結(jié)合Doug自己說法,他覺得允許為null的設(shè)計是不合理的:(他這里寫的nulls,我理解是key和value都不能為null。)


file

到底怎么答?

所以,對于文章開頭拋出的問題,怎么回答?

如果面試官問的是為什么ConcurrentHashMap的value不能為null?這樣的面試題還是有意義的,因為你還能和他掰扯掰扯二義性。說明你對ConcurrentHashMap有一定的思考。

但是面試官問出的為什么concurrentHashMap的key不能為null?像我文章開頭的寫那樣,看完這幾封郵件后我還是不知道怎么回答。

我能怎么回答?

我回答源碼就是這樣寫的?一句話的回答,面試官不太滿意。那我說因為作者Doug不喜歡null,所以在設(shè)計之初就不允許了null的key存在。如果面試官期望的這樣的回答,這題會不會有點太偏了?

所以我覺得這題當(dāng)奇聞軼事可以,但是要強行當(dāng)作面試題,我覺得有點牽強了吧。

file

最后說一點

這篇文章,提煉出來的知識點是一個很小的點,但是為什么我又洋洋灑灑的寫了7000多字呢?

因為我覺得提煉出來的,是一個干癟癟的知識點,它不夠豐富,沒有探索的過程。

而我所展示的是我去尋找這個問題的答案的過程。通過四封郵件內(nèi)容,把前因后果串聯(lián)起來,而且是作者的親自回答,極具權(quán)威性。

這篇文章不僅鍛煉了我的邏輯推理能力,還鍛煉了我的英語翻譯能力,對我自己是一個很大的幫助。

我永遠是我文章的第一讀者,我覺得好的,對我有很大幫助的東西我才會去寫。因為對我有很大幫助的東西,多少對你能有一點幫助。

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

如果你覺得文章還不錯,你的轉(zhuǎn)發(fā)、分享、贊賞、點贊、留言就是對我最大的鼓勵。

感謝您的閱讀,感謝您的關(guān)注。

以上。

歡迎關(guān)注公眾號【why技術(shù)】。在這里我會分享一些技術(shù)相關(guān)的東西,主攻java方向,用匠心敲代碼,對每一行代碼負責(zé)。偶爾也會荒腔走板的聊一聊生活,寫一寫書評,影評。愿你我共同進步。

公眾號-why技術(shù)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容