是否应该允许在Java中将HashSet添加到自身?

时间:2018-04-19 15:41:41

标签: java collections set hashset contract

根据Java中的Set的合同,“不允许集合将自己包含为元素”(source)。但是,对于HashSet of Objects,这是可能的,如下所示:

Set<Object> mySet = new HashSet<>();
mySet.add(mySet);
assertThat(mySet.size(), equalTo(1));

这个断言通过了,但是我希望行为是将结果集合设为0或抛出异常。我意识到HashSet的底层实现是一个HashMap,但似乎应该在添加元素之前进行相等检查以避免违反该合同,不是吗?

4 个答案:

答案 0 :(得分:52)

其他人已经从数学的角度指出了为什么它是值得怀疑的,提到Russell's paradox

但是,这不会在技术级别上回答您的问题。

让我们解读一下:

首先,再次来自JavaDoc of the Set interface:

的相关部分
  

注意:如果将可变对象用作set元素,则必须非常小心。如果在对象是集合中的元素的同时以影响等于比较的方式更改对象的值,则不指定集合的​​行为。这种禁令的一个特例是,不允许集合将自身作为一个要素包含在内。

有趣的是,JavaDoc of the List interface提出了类似的,虽然有点弱,但同时还有更多的技术陈述:

  

虽然列表允许将自己包含为元素,但建议极为谨慎:equalshashCode方法不再在此列表中明确定义。

最后,症结在JavaDoc of the Collection interface,这是SetList界面的共同祖先:

  

执行集合递归遍历的某些集合操作可能会失败,自引用实例的例外情况是集合直接或间接包含自身。其中包括clone()equals()hashCode()toString()方法。实现可以可选地处理自引用场景,但是大多数当前实现不这样做。

(由我强调)

大胆的部分暗示了为什么你在问题中提出的方法是不够的:

  

似乎应该在添加元素之前进行相等检查以避免违反该合同,不是吗?

这对你没有帮助。关键是当集合直接或间接包含自身时,您总会遇到问题。想象一下这种情况:

Set<Object> setA = new HashSet<Object>();
Set<Object> setB = new HashSet<Object>();
setA.add(setB);
setB.add(setA);

显然,这两个集合都不包含直接。但是它们中的每一个都包含另一个 - 因此,它本身间接地。通过简单的引用相等性检查(使用==方法中的add)无法避免这种情况。

避免这种&#34;不一致的状态&#34;在实践中基本上不可能。当然,理论上可以使用参考Reachability计算。事实上,垃圾收集器基本上必须这样做!

但是当涉及自定义类时,在实践中变得不可能。想象一下这样的课程:

class Container {

    Set<Object> set;

    @Override 
    int hashCode() {
        return set.hashCode(); 
    }
}

乱搞这个及其set

Set<Object> set = new HashSet<Object>();
Container container = new Container();
container.set = set;
set.add(container);

add的{​​{1}}方法基本上无法检测在那里添加的对象是否有某些(间接)引用到集合本身。

长话短说:

你不能阻止程序员弄乱。

答案 1 :(得分:22)

将集合添加到自身一次会导致测试通过。添加两次会导致您正在寻找的StackOverflowError

从个人开发人员的角度来看,强制检查底层代码以防止这种情况是没有任何意义的。如果您尝试执行此操作太多次,或者计算StackOverflowError - 这将导致即时溢出 - 您的代码中得到hashCode这一事实应该足以确保没有理智的开发人员会将这种代码保存在代码库中。

答案 2 :(得分:12)

您需要阅读完整的文档并完整引用它:

  

如果在对象是集合中的元素时,以影响等于比较的方式更改对象的值,则未指定集合行为 。此禁令的特殊情况是,集合不允许将自身包含为元素。

实际限制在第一句中。如果集合的元素发生变异,则行为未指定

由于将一个集合添加到自身会使其发生变异,并再次添加它会使其再次变异,结果未指定。

请注意,限制是行为未指定,并且该限制的特殊情况正在将该集添加到自身。

因此,文档说,换句话说,向自身添加一个集合会导致未指定的行为,这就是您所看到的。它取决于(或不)处理的具体实现。

答案 3 :(得分:8)

我同意你的看法,从数学的角度来看,这种行为确实没有意义。

这里有两个有趣的问题:首先,Set接口的设计者在多大程度上试图实现数学集?其次,即使他们不是,它在多大程度上豁免了他们的集合论规则?

对于第一个问题,我将引导您到集合的documentation

  

不包含重复元素的集合。更正式地说,集合不包含元素对e1和e2,使得e1.equals(e2)和至多一个null元素。 正如其名称所暗示的,此界面模拟数学集抽象。

这里值得一提的是,目前的集理论公式不允许集合成为自己的成员。 (见Axiom of regularity)。这部分归因于Russell's Paradox,它在naive set theory中暴露了一个矛盾(允许一个集合是任何对象的集合 - 没有禁止包括他们自己的集合) 。这通常由Barber Paradox说明:假设在一个特定的城镇,理发师会刮掉所有的男人 - 而只是男人 - 他们不会刮胡子。问题:理发师自己刮胡子吗?如果他这样做,则违反了第二个约束条件;如果他不这样做,则违反了第一个约束。这显然在逻辑上是不可能的,但实际上在天真集合理论的规则下是完全允许的(这就是为什么新的“标准”集合论明确禁止集合包含自己)。

this question on Math.SE中有关于为什么集合不能成为其自身元素的更多讨论。

话虽如此,这提出了第二个问题:即使设计师没有明确地试图建模数学集,这是否完全“免除”与天真相关的问题集理论?我认为不是 - 我认为困扰幼稚集合理论的许多问题会困扰任何类型的集合,这些集合在类似于天真集合理论的方式上受到不充分的约束。实际上,我可能会对此进行过多的阅读,但文档中Set定义的第一部分听起来像天真集理论中的集合的直观概念:

  

不包含重复元素的集合。

不可否认(并且他们相信),他们稍后会对此进行至少一些约束(包括说明你真的不应该尝试让Set包含它自己),但是你可以提出疑问是否真的“足够”以避免天真集理论的问题。这就是为什么,例如,当您尝试计算包含自身的HashSet的哈希码时,您有一个“乌龟一直向下”的问题。正如其他人所说的那样,这不仅仅是一个实际问题 - 它是这类公式的基本理论问题的例证。

作为一个简短的题外话,我确实认识到,任何集合类对数学集的真实建模有多么接近。例如,Java的文档警告在集合中包含可变对象的危险。其他一些语言,比如Python,至少尝试ban many kinds of mutable objects entirely

  

使用词典实现集合类。因此,对集合元素的要求与字典键的要求相同;即,该元素定义__eq__()__hash__()因此,集合不能包含可变元素,例如列表或字典。但是,它们可以包含不可变集合,例如元组或ImmutableSet实例。为了方便实现集合,内部集合会自动转换为不可变形式,例如Set([Set(['dog'])])转换为Set([ImmutableSet(['dog'])])

其他人指出的另外两个主要差异是

  • Java集是可变的
  • Java集是有限的。显然,对于任何集合类都是如此:除了对actual infinity的担忧之外,计算机只有有限的内存量。 (有些语言,比如Haskell,有懒惰的无限数据结构;但是,在我看来,lawlike choice sequence似乎比经典集理论更自然地模拟这些,但这只是我的观点。)

TL; DR 不,它确实不应该被允许(或者,至少,你永远不应该这样做),因为集合不能成为他们自己的成员。