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


​持續輸出原創文章,關注我吧

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

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

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

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

送分題?送命題?

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

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

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

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

面試官:沒了?

面試者:沒了。

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

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

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

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

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

SHOW ME THE CODE

我們先看一下當ConcurrentHashMap的key和value分別都為null的時候,程序的執行結果是什么:

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

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

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

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

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

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

 

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

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

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

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

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

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

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

第一封:Tutika求助

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

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

郵件內容如下:

 

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

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

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

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

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


翻譯結束。

這里我想插個題外話,關於提問的藝術,我覺得Tutika同學的提問方式就很標准。在什么場景下遇到了什么問題,自己嘗試的解決方案是什么,請問有沒有更好的解決方案?

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

第二封:熱心網友

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

Tutika發出"求救"郵件后的1小時20分18秒,就有熱心網友Holger回復了他的問題,

原版全文如下

我再來翻譯一下:

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

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

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

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

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

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

Tutika:在我的應用程序中,對於值為null的value和key是非常難以判斷的。

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

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

Holger:即使這樣,你仍然會遇到這樣的問題:首先你需要找到現有Map的構造函數的所有調用方並修復它們。而且這也是不可能的,比如你有可能是從其他地方獲取到這個Map的。

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

Holger給出了下面兩個選擇:

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

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

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

翻譯結束。

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

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

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

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

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

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

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

第三封:巨佬現身

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

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

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

翻譯一下:

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

對於熱心網友Holger的郵件,Doug說:你可以試着接受Holger的建議,雖然他都沒有說到點子上...

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

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

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

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

翻譯結束。

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

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

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

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

2.這個key的value在設置的時候,就是null。

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

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

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

輸出的結果為:

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

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

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

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

這時有A、B兩個線程。

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

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

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

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

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

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

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

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

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

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

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

第四封郵件:Josh回應

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

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

郵件內容如下:

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

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

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

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

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

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

到底怎么答?

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

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

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

我能怎么回答?

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

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

最后說一點

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

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

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

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

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


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

感謝您的閱讀,感謝您的關注。

以上。

持續輸出原創


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM