本文是自己研究的,如果有任何疑问和补充,请您在评论区指正好嘛,我会立刻修改,以免误导他人.
如果您觉得本文质量不错,可以点个赞和关注一下好嘛.
如果您需要引用本文章的内容,请附加链接,谢谢.
问题描述:
为什么HashTable,ConcurrentHashMap不能保存null键和null值
为什么ArrayList ,LinkedList,甚至是HashMap都允许加入null键和值
博客概述
本文从设计思路到源码进行讲解.下面是概述
HashMap存在二义性 ----> ConcurrentMap,HashTable为什么不存在二义性 ---- > 二义性由 返回值 null ,不能代表元素的存在或者不存在引起 ----> 引申部分 : ArrayList,LinkedList 存在不存在二义性
首先要了解HashMap存在二义性.
HashMap能存储key == null, 也能存储 value == null
二义性是什么先看 get方法,get返回值有两种,一种是 key的值, 还有一种就是 null ,null 有两种含义,不存在该key 以及 存在该 key,但是存的值为 null,这就是二义性
下面来看方法
public V get(Object key) {Node e;// 这里是关键哦,getNode后,会返回Node节点(里面保存了k-v),也就是说不存在的节点,返回的是null,没有异议吧.// 如果存在呢,也就是找到了一个e ,e != null ,e里面保存了k - v, 也就是返回 e.value 没问题吧,那么假如我value就是null 呢// 这里就出现了,如果没有这个Node,那么e == null,如果有Node,但是 value == null,返回的也是 null .//二义性就是,寻找Node的时候,找到了,其value == null,就返回null,找不到也返回null,有没有这个元素,都有可能返回 null,这就是二义性,即 null 代表两种状态,有歧义return (e = getNode(hash(key), key)) == null ? null : e.value;}
这里很简单,就是找到了返回元素,没找到返回null
final Node getNode(int hash, Object key) {Node[] tab; Node first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;if ((e = first.next) != null) {if (first instanceof TreeNode)return ((TreeNode)first).getTreeNode(hash, key);do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;}
看到上面,你会觉得,二义性就是 通过 get() 方法的返回值,如果返回值是 null 无法判断出是否一定存在该元素.
但是,如果你觉得这是他设计的不好,或者是我讲错了.那你想想,这个"Bug"能解决吗,是否可以通过 contains(key) 方法,也就是说可以通过 contains 来判断是否存在该 key 行不行.
来看看contains方法,
这里可以解决二义性的,因为这里返回的是 boolean ,存在即存在,不存在即 false
public boolean containsKey(Object key) {return getNode(hash(key), key) != null;}
public boolean containsValue(Object value) {Node[] tab; V v;if ((tab = table) != null && size > 0) {for (int i = 0; i < tab.length; ++i) {for (Node e = tab[i]; e != null; e = e.next) {if ((v = e.value) == value ||(value != null && value.equals(v)))return true;}}}return false;}
有趣的理解:
当 get(key) 的结果为 null 时, 该 key 的存在或者不存在问题类似于薛定谔的猫(如果key存在,这个null就是key的值,如果key不存在,这个 null 只代表了不存在这个key),只有你 containsKey() 之后,才能判断存在这个 key
案例演示:
public class HashMapTest {public static void main(String[] args) {HashMap map = new HashMap<>();System.out.println("put方法,该方法返回null代表的是没有重复的key, 非null是返回的代表被覆盖的值,结果" + map.put("ggzx", null));System.out.println("get方法返回值,null 可以代表值为 null,也可能代表 不存在,我尝试获取,但结果 " + map.get("ggzx"));System.out.println(map.containsKey("ggzx"));System.out.println(map.containsValue(null));}
}
结果:
put方法,该方法返回null代表的是没有重复的key, 非null是返回的代表被覆盖的值,结果null
get方法返回值,null 可以代表值为 null,也可能代表 不存在,我尝试获取,但结果 null
true
true
看一看第二个输出:
如果你知道自己插入了数据,此时null代表了什么
如果你不知道自己插入了什么数据,这个时候你可以断定, null 是值 还是代表不存在.
如果你调用 cotainsKeys() ,能否能判断存在该key吗?
为什么有二义性还要搞
你已经了解了HashMap其实是存在二义性,但是也能够解决,在能解决的基础上,能保存 null-null,感觉也还行对吧.至少能表达一种状态,比如说存储 className --> null ,的情况,null 能代表不存在该 class
为啥,因为设计者都说了啊,他不想在并发过程中存在这个二义性问题!!!,不过这里我还想冒昧的看看,如果能保存,是否能够用containsKey()来解决
首先来看,Concurrent和HashTable 不能存储 null - null 的证据哇
ConcurrentHashMap#V putVal(K key, V value, boolean onlyIfAbsent)
final V putVal(K key, V value, boolean onlyIfAbsent) {// 对于ConcucrrentHashMap, 如果 key || value == null,都会直接判处if (key == null || value == null) throw new NullPointerException();...本文讲null的存储的,又不讲源码,不给你看很多return null;
public synchronized V put(K key, V value) {// 如果value == null ,直接抛出异常if (value == null) {throw new NullPointerException();}// Makes sure the key is not already in the hashtable.Entry,?> tab[] = table;// 如果 key == null,就抛出异常了,不过我也不知道为什么不检测,而需要运行时遇到异常抛出去int hash = key.hashCode();... 这部分不给你看了addEntry(hash, key, value, index);return null;}
好了,目前为止,你已经了解了,他们俩,都不能存储 key == null 或者 value == null.
不能存储 null ,返回值如果为 null ,肯定代表不存在了对吧,此时就没有二义性
先看看ArrayList# E get(int index)
这里压根就不存在二义性哇,我传入一个索引,索引只有两个状态,无效的和有效的.结果不存在二义性,如果返回 null 呢?代表的就是存储的 null 呗,没有 null 代表不存在一说
public E get(int index) {// 检查是否越界rangeCheck(index);// 直接返回return elementData(index);}
再看看ArrayList#boolean add(E e)
仔细看看返回的是啥,boolean,加入成功就是成功,失败就是失败,也不会返回null.
第二个重载方法,直接 void 了,还有啥二义性可言
// 加入元素public boolean add(E e) {ensureCapacityInternal(size + 1); // Increments modCount!!elementData[size++] = e;return true;}// 重载方法public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size + 1); // Increments modCount!!System.arraycopy(elementData, index, elementData, index + 1,size - index);elementData[index] = element;size++;}
下面再看看 LinkedList 吧
存入null之前之后,都是返回的 null ,也无法判断,是否是存在key呢,还是不存在key呢
public class LinkedListTest {public static void main(String[] args) {LinkedList list = new LinkedList<>();System.out.println(list.peekLast());list.add(null);list.add(null);list.add(null);list.add(null);System.out.println(list.peekLast());}
}
结果:
null
null
最主要的前面的部分,即HashTable,ConcurrentHashMap和HashMap,后面的可以用于拓展吧.
如果你有问题,您可以在评论区进行评论.
你都看到这里了,不打算点个赞和关注嘛