持續輸出原創文章,關注我吧html
面試是一個很奇怪的過程,都是擰螺絲的。可是問的都是如何造火箭,一個敢問,一個敢答。java
面試不可怕,可怕的是你get不到面試官的點。面試
更可怕的是,你以爲你知道答案,但不是面試官想要的。編程
最可怕的是,面試官也不知道這題的答案是什麼。安全
前段時間有個小夥伴在一個羣裏分享了一道親身經歷的面試題,這題乍一看好像張口就能答,可是仔細一想,面試官是想要這樣的回答嗎?具體能夠看截圖。多線程
能夠想象一下那個略顯尷尬的畫面:併發
面試官:請問ConcurrentHashMap中的key爲何不能爲null?函數
面試者:由於源碼裏面就是這樣寫的,判斷爲空,拋出異常。單元測試
面試官:沒了?測試
面試者:沒了。
我前思後想,對於這個問題我是真的不知道面試官想要什麼樣的答案。就算我寫完這篇文章以後,我知道了來龍去脈,我仍是不清楚怎麼回答他的這個問題。由於我get不到他的點在哪裏。
具體怎麼回事,看完本文以後,你就知道了。
我提煉並昇華一下這個面試題,請問:
ConcurrentHashMap爲何不能存值爲null的value?
ConcurrentHashMap爲何不能放值爲null的key?
咱們先看一下當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的問題寫過郵件諮詢過,而他老爺子親自回答了這個問題。
本文在翻譯四封相關郵件的過程當中,結合老爺子的郵件,加上本身的理解來回答這個問題。
說明:本人英文水平有限,翻譯出來的文章你們看的時候多多包涵。同時我也附上原文和郵件地址,你們能夠訪問。
郵件地址: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是頗有發言權的。
並且,啊,爲何他這麼強,也有這麼多頭髮。
郵件地址: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多字呢?
由於我以爲提煉出來的,是一個乾癟癟的知識點,它不夠豐富,沒有探索的過程。
而我所展現的是我去尋找這個問題的答案的過程。經過四封郵件內容,把來龍去脈串聯起來,並且是做者的親自回答,極具權威性。
這篇文章不只鍛鍊了個人邏輯推理能力,還鍛鍊了個人英語翻譯能力,對我本身是一個很大的幫助。
我永遠是我文章的第一讀者,我以爲好的,對我有很大幫助的東西我纔會去寫。由於對我有很大幫助的東西,多少對你能有一點幫助。
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。
感謝您的閱讀,感謝您的關注。
以上。