1. 程式人生 > >這道面試題我真不知道面試官想要的回答是什麼

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

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

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

面試不可怕,可怕的是你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多字呢?

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

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

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

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


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

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

以上。

持續輸出原創