为什么 ImmutableCollection.contains(null) 会失败?

Why does ImmutableCollection.contains(null) fail?

前面的问题: 为什么在 Java 中对 ImmutableCollections 的调用 coll.contains(null) 失败?

我知道,不可变集合不能包含空元素,我不想讨论这是好是坏。

但是当我编写一个采用(一般的,不是显式不可变的)集合的函数时,它在检查空值时失败。为什么实施不是 return false(实际上是 'correct' 答案)?

一般情况下,如何正确检查 Collection 中的空值?

编辑: 通过一些讨论(感谢评论者!)我意识到,我混淆了两件事:ImmutableCollection from the guava library, and the List returned by java.util.List.of,来自 ImmutableCollections 的 class。然而,两个 classes 在 .contains(null).

上抛出 NPE

我的问题是 List.of 结果,但从技术上讲,guaves 实施也会发生同样的情况。 [编辑:它没有]

why does in Java the call coll.contains(null) fail for ImmutableCollections?

因为设计团队(创建番石榴的人)决定,对于他们的集合,null 是不需要的,因此他们的集合和 null 检查之间的任何交互,即使在这种情况下,也应该尽早向程序员强调存在不匹配。即使已建立的行为(根据核心运行时本身的现有实现,例如 ArrayList 和朋友,以及 javadoc),也明确地采用另一种方式并说不合逻辑的检查(这个梨是这个苹果列表的一部分吗?)强烈建议正确的做法是 return false 而不是扔。

换句话说,番石榴搞砸了。但既然他们已经这样做了,回去可能会破坏向后兼容性。它真的不是很好——你正在用 false return 值替换抛出的异常;大概代码可能在那里依赖于 NPE(捕获它并做一些不同于代码所做的事情 contains(null) returned false 而不是抛出) - 但这是一种罕见的情况,番石榴总是破坏向后兼容性。

And how can I properly check for nulls in a Collection in general?

通过调用 .contains(null),就像你一样。番石榴不正确的事实并没有改变答案。您不妨问 'how do I add elements to a list',然后反驳“好吧,您调用 list.add(item) 来做到这一点”的答案:好吧,我有一个 List 接口的实现,它通过扬声器播放 Rick Astley添加到列表中,所以,我拒绝你的回答。

那是.. java 和接口的工作方式:您可以拥有它们的实现,并且它们按照接口指示它们必须执行的操作的唯一监护人是作者了解有一个合同需要待后续。

现在,通常一个写得如此糟糕以至于无缘无故违约*的图书馆并不受欢迎。但是番石榴很受欢迎。很受欢迎。这说明了一个简单的事实:没有图书馆是完美的。 Guava 的 API 设计通常相当不错(在我看来,大大 优于例如 Apache 公共库),并且团队积极花费大量时间讨论适当的 API 设计,从某种意义上说,使用番石榴编写的代码很好(定义为:易于理解,几乎没有惊喜,易于维护,易于测试,并且可能易于变异以应对不断变化的需求 - 的仅对 'nice' 或 'elegant' 代码等模糊术语的有用定义 - 它是执行这些操作的代码,其他任何内容都是毫无意义的审美废话)。换句话说,他们在积极尝试,而且通常都做对了。

只是,不是在这种情况下。解决它:return item != null && coll.contains(item); 将完成工作。

有一个支持番石榴选择的主要论点:它们 'contract break' 是一种隐含的中断 - 人们会期望 .contains(null) 有效,并且总是 return 是错误的,但它是不是 明确地 在 java 文档中声明必须这样做。对比例如IdentityHashMap,它在其 .containsKey 等实现中使用身份等价(a==b)而不是值等价(a.equals(b)),明确地违反 j.u.Map 界面中所述的 javadoc 合同。 IHM 对此有很好的理由,并在 java 文档中强调了差异,并解释了原因。 Guava 对它们奇怪的 null 行为几乎没有那么清楚,但是,java:

中关于 null 的一个关键问题

意义不明。有时它意味着 'empty',这是糟糕的设计:你永远不应该写 if (x == null || x.isEmpty()) - 这意味着某些 API 编码错误。如果 null 在语义上等同于某个值(例如 ""List.of()),那么您应该只是 return ""List.of(),而不是 null。然而,在这样的设计中,list.contains(null) == false) 是有意义的。

但有时 null 表示 not foundirrelevantnot applicableunknown(例如,如果 map.get(k) return 为 null ,这就是它的意思:未找到。不是 'I found an empty value for you')。这与 NULL 在例如中的含义相匹配。 SQL。在所有这些情况下,.contains(null) 应该 return 既不正确也不错误。如果我递给你一袋弹珠并问你里面有没有一颗弹珠是灰色的,而你不知道 grue 是什么意思,你不应该回答 yesno 我的查询:任何一个答案都是毫无意义的猜测。你应该告诉我这个问题无法回答。在 java 中,最好的表现是投掷,这正是番石榴所做的。这也与 NULL 在 SQL 中的作用相匹配。在 SQL、v IN (x) return 中,是 3 个值之一,而不是 2 个值:它可以解析为 truefalsenullv IN (NULL) 将解析为 NULL 而不是 false。它是在回答一个不能用NULL值回答的问题,读作:不知道。

换句话说,番石榴调用了 null 暗示的内容,这显然与您的定义不匹配,正如您期望 .contains(null) 到 return 错误。我认为你的观点比较地道,但重点是,番石榴的观点不同但又一致,javadoc只是影射,但没有明确要求,.contains(null) returns假的。

这对修复您的代码没有任何用处,但希望它能为您提供一个心智模型,并回答您“为什么它会这样工作?”的问题。

这个讨论我很心疼!

在我编写第一个最终成为 Guava 的集合之前,执行此操作的集合一直是我的宠儿。如果您发现任何 Guava 集合抛出 NPE 只是因为您问了一个完全无辜的问题,例如 .contains(null),请提交错误!我们讨厌那些废话。

编辑:我非常苦恼,以至于我不得不回去查看我的 2007 年变更列表,该变更列表首先创建了 ImmutableSet 并从字面上看到了这个:

  @Override public boolean contains(@Nullable Object target) {
    if (target == null) {
      return false;
    }

啊啊啊。