这是why技术的第15篇原创文章
面试是一个很奇怪的过程,都是拧螺丝的。但是问的都是如何造火箭,一个敢问,一个敢答。
面试不可怕,可怕的是你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多字呢?
因为我觉得提炼出来的,是一个干瘪瘪的知识点,它不够丰富,没有探索的过程。
而我所展示的是我去寻找这个问题的答案的过程。通过四封邮件内容,把前因后果串联起来,而且是作者的亲自回答,极具权威性。
这篇文章不仅锻炼了我的逻辑推理能力,还锻炼了我的英语翻译能力,对我自己是一个很大的帮助。
我永远是我文章的第一读者,我觉得好的,对我有很大帮助的东西我才会去写。因为对我有很大帮助的东西,多少对你能有一点帮助。
才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。
如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
感谢您的阅读,感谢您的关注。
以上。
欢迎关注公众号【why技术】。在这里我会分享一些技术相关的东西,主攻java方向,用匠心敲代码,对每一行代码负责。偶尔也会荒腔走板的聊一聊生活,写一写书评,影评。愿你我共同进步。