为什么 Map.of 不允许空键和值?

在 Java9中,为 ListSetMap接口引入了新的工厂方法。这些方法允许用一行中的值快速实例化 Map 对象。现在,如果我们考虑:

Map<Integer, String> map1 = new HashMap<Integer, String>(Map.of(1, "value1", 2, "value2", 3, "value3"));
map1.put(4, null);

以上各项均无例外,但如属下列情况,则属例外:

Map<Integer, String> map2 = Map.of(1, "value1", 2, "value2", 3, "value3", 4, null );

它抛出:

Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:221)
..

我不能得到,为什么不允许 null在第二种情况下。

我知道 HashMap 可以将 null 作为键和值,但是为什么在 Map.of 的情况下会受到限制呢?

同样的事情发生在 java.util.Set.of("v1", "v2", null)java.util.List.of("v1", "v2", null)的情况下。

61861 次浏览

并非所有的 Maps 都允许空作为键

docs of Map中提到的原因。

Some map implementations have restrictions on the keys and values they may contain. For example, some implementations prohibit null keys and values, and some have restrictions on the types of their keys. Attempting to insert an ineligible key or value throws an unchecked exception, typically NullPointerException or ClassCastException.

正是-一个 HashMap被允许存储 null,没有Map由静态工厂方法返回。不是 所有地图是相同的。

一般来说,就我所知,首先在 HashMap中允许空值作为键是错误的,新的集合禁止这种可能性。

想象一下这样的情况: 在 HashMap中有一个条目具有某个键并且 value = = null。你得到,它返回 null。什么意思?它有一个 null的映射,还是它不存在?

Same goes for a Key - hashcode from such a null key has to treated specially all the time. Banning nulls to start with - make this easier.

虽然 HashMap 允许空值,但是 Map.of不使用 HashMap,如果用作键或值,则抛出一个异常,如 记录在案:

Map.of ()和 Map.ofEntry ()静态工厂方法提供了创建不可变映射的便利方法。这些方法创建的 Map 实例具有以下特征:

  • ...

  • 它们不允许空键和值。尝试使用空键或值创建它们会导致 NullPointerException。

正如其他人指出的,Map合同允许拒绝空值..。

有些实现禁止 null键和值[ ... ]。试图插入不合格的键或值将引发未检查的异常,通常为 NullPointerExceptionClassCastException

... 和收集工厂(不只是在地图上) 好好利用这一点

它们不允许 null键和值。尝试使用 null键或值创建它们将导致 NullPointerException

但是为什么呢?

到目前为止,允许在集合中使用 null被视为一个设计错误。这有很多原因。一个好的例子是可用性,其中最突出的麻烦制造者是 Map::get。如果返回 null,则不清楚键是否丢失或值是否为 null。一般来说,保证免费使用 null的集合更容易使用。在实现方面,它们还需要更少的特殊大小写,使代码更容易维护,性能更好。

你可以听斯图尔特 · 马克斯(Stuart Marks)解释 在这个演讲中,但是 JEP 269(介绍工厂方法的那个)也总结了它:

不允许使用空元素、键和值。(最近引入的集合都不支持 null。)此外,禁止 null 提供了更紧凑的内部表示、更快的访问速度和更少的特殊情况的机会。

因为当 HashMap慢慢被发现的时候,它已经在野外了,所以在不破坏现有代码的情况下修改它已经太晚了,但是这些接口的最新实现(例如 ConcurrentHashMap)不再允许 null了,工厂方法的新集合也不例外。

(我认为另一个原因是显式使用 null值被认为是一个可能的实现错误,但我错了。这是为了复制钥匙,这也是非法的。)

So disallowing null had some technical reason but it was also done to improve the robustness of the code using the created collections.

文件没有说 为什么 null是不允许的:

它们不允许 null键和值。尝试使用 null键或值创建它们将导致 NullPointerException

在我看来,要生成常量的 Map.of()Map.ofEntries()静态工厂方法大多由编译类型的开发人员构成。那么,保留 null作为键或值的原因是什么?

Whereas, the Map#put is usually used by filling a map at runtime where null keys/values could occur.

主要的区别是: 当你建立自己的地图的“选项1”的方式... 然后你是 毫无疑问说: “我想有完整的 自由在我正在做的”。

因此,当您决定您的映射应该有一个空键或值(可能是由于列出的原因 here) ,然后您可以自由地这样做。

But "option 2" is about a convenience thing - probably 打算 to be used for constants. And the people behind Java simply decided: "when you use these convenience methods, then the resulting map shall be null-free".

允许空 价值观意味着

 if (map.contains(key))

是不一样的

 if (map.get(key) != null)

这有时可能是个问题。或者更准确地说: 在处理这个映射对象时需要记住它。

这只是一个为什么这似乎是一个合理的方法的小道消息: 我们的团队自己实现了类似的方便方法。你猜怎么着: 在不知道任何关于未来 Java 标准方法的计划的情况下,我们的方法做完全相同的事情: 它们返回传入数据的一个不可变副本; 当你提供 null 元素时,它们就会抛出。我们甚至非常严格,当你通过 空荡荡的列表/地图/... 我们也抱怨。

在映射中允许空值是一个错误。我们可以看到它的 现在,但我猜它不清楚当 HashMap被介绍。NullpointerException是在生产代码中看到的 most common bug

我想说的是,JDK 正朝着帮助开发人员战胜 NPE 瘟疫的方向发展:

  • Introduction of Optional
  • Collectors.toMap(keyMapper, valueMapper) doesn't allow neither the keyMapper nor the valueMapper function to return a null value
  • 如果找到的值是 null,则 Stream.findFirst()Stream.findAny()抛出 NPE

因此,我认为禁止在新的 JDK9不可变集合(和映射)中使用 null也是同样的道理。

在我看来,非空性对于键来说是有意义的,但对于值来说就不是了。

  1. Map接口变成了一个弱契约,如果不查看实现,就不能信任它的行为,而且前提是您有权查看它。Java11的 Map.of()不允许空值,而 HashMap允许,但它们都实现了相同的 Map协议-为什么?
  2. 拥有一个空值或者根本没有值是不可能的,而且可以说也不应该被看作是一个独特的场景。当你得到一个键的值并得到 null 时,谁会在乎它是否被显式地放在那里?根本没有提供的键值,映射不包含这样的键,就这么简单。Null 为 Null,没有 Null 或 Null ^ 2
  3. 现有的库大量使用 map 作为向它们传递属性的方法,其中许多属性是可选的,这使得 Map.of()作为这类结构的构造毫无用处。
  4. Kotlin 在编译时强制执行空性,并允许带有空值的映射没有已知问题。
  5. 不允许空值的原因可能仅仅是由于实现的细节和创建者的方便。