为什么Java toString()在间接循环中无限循环?

时间:2009-06-15 10:49:54

标签: java collections cycle infinite-loop

这更像是我想分享的问题而不是问题:当使用toString()进行打印时,Java将检测Collection中的直接循环(Collection指向自身),但不是间接循环(其中Collection是指另一个集合,它指的是第一个 - 或者更多的步骤)。

import java.util.*;
public class ShonkyCycle {
  static public void main(String[] args) {
    List a = new LinkedList();
    a.add(a);                      // direct cycle
    System.out.println(a);         // works:  [(this Collection)]

    List b = new LinkedList();
    a.add(b);
    b.add(a);                      // indirect cycle
    System.out.println(a);         // shonky:  causes infinite loop!
  }
}

对我来说这是一个真正的问题,因为它发生在调试代码中以打印出Collection(当它遇到直接循环时我感到很惊讶,所以我错误地认为他们已经实现了一般的检查)。有一个问题:为什么?

我能想到的解释是,检查引用自身的集合是非常便宜的,因为您只需要存储集合(您已经拥有),但是对于更长的周期,您需要存储所有您遇到的集合,从根开始。此外,您可能无法确定 根是什么,因此您必须将每个集合存储在系统中 - 无论如何都要存储 - 但您还必须这样做每个集合元素的哈希查找。对于相对罕见的周期(在大多数编程中),这是非常昂贵的。 (我认为)它检查直接周期的唯一原因是因为它如此便宜(一个参考比较)。

好的...我有点回答了我自己的问题 - 但是我错过了什么重要的事吗?有人想要添加任何内容吗?


澄清:我现在意识到我看到的问题特定于打印一个集合(即toString()方法)。循环本身没有问题(我自己使用它们并需要它们);问题是Java无法打印它们。 编辑 Andrzej Doyle指出它不仅仅是集合,而是任何调用toString的对象。

鉴于它受此方法的限制,这里有一个检查它的算法:

  • root是调用第一个toString()的对象(为了确定这一点,你需要保持关于toString当前是否正在进行的状态;所以这很不方便)。
    • 在遍历每个对象时,将其添加到IdentityHashMap以及唯一标识符(例如递增的索引)。
    • 但如果此对象已在Map中,请改为写出其标识符。

此方法还可以正确呈现multirefs(多次引用的节点)。

内存成本是IdentityHashMap(每个对象一个引用和索引);复杂性成本是有向图中每个节点(即每个打印的对象)的哈希查找。

5 个答案:

答案 0 :(得分:5)

我认为从根本上说这是因为虽然语言试图阻止你在脚下射击,但它不应该以一种昂贵的方式这样做。因此,虽然比较对象指针(例如obj == this)几乎是免费的,但除此之外的任何事情都涉及调用您传入的对象上的方法。

此时,库代码对您传入的对象一无所知。例如,泛型实现不知道它们是Collection的实例(或{{1 }})本身,虽然它可以通过Iterable找到它,谁会说它是否是一个“类似集合”的对象,它实际上不是一个集合,但仍然包含一个延迟的循环引用?其次,即使它是一个集合,也不知道它的实际实现是什么,因此行为就像。理论上,人们可以拥有一个包含所有Longs的集合,这些集合将被懒惰地使用;但由于图书馆不知道这一点,因此迭代每个条目会非常昂贵。或者实际上甚至可以设计一个永远不会终止的迭代器集合(虽然这在实践中很难使用,因为很多构造/库类假设instanceof最终会返回hasNext)。 / p>

所以它基本上归结为一个未知的,可能是无限的代价,以阻止你做一些可能实际上不是问题的事情。

答案 1 :(得分:3)

我只想指出这句话:

  

使用toString()打印时, Java将在集合中检测直接循环

具有误导性。

Java (JVM,语言本身等)未检测到自引用。相反,这是toString()方法/覆盖java.util.AbstractCollection的属性。

如果您要创建自己的Collection实现,语言/平台不会自动保护您免受这样的自我引用 - 除非您扩展AbstractCollection,否则您必须确保你自己掩盖了这个逻辑。

我可能会在这里分裂,但我认为这是一个重要的区别。仅仅因为JDK中的一个基础类做了某些事情并不意味着“Java”作为一个整体的保护伞。

以下是AbstractCollection.toString()中的相关源代码,其中的关键行已注明:

public String toString() {
    Iterator<E> i = iterator();
    if (! i.hasNext())
        return "[]";

    StringBuilder sb = new StringBuilder();
    sb.append('[');
    for (;;) {
        E e = i.next();
        // self-reference check:
        sb.append(e == this ? "(this Collection)" : e); 
        if (! i.hasNext())
            return sb.append(']').toString();
        sb.append(", ");
    }
}

答案 2 :(得分:1)

您建议的算法问题是您需要将IdentityHashMap传递给所涉及的所有集合。使用已发布的Collection API无法做到这一点。 Collection接口未定义toString(IdentityHashMap)方法。

我想,无论是谁,Sun都会将自我参考检查纳入考虑所有这一切的AbstractCollection.toString()方法中,并且(与他的同事一起)决定“整体解决方案”超越顶部。我认为目前的设计/实施是正确的。

并不要求Object.toString实现是防弹的。

答案 3 :(得分:0)

你是对的,你已经回答了自己的问题。检查更长的周期(特别是长周期,例如周期长度1000)将是过多的开销,并且在大多数情况下不需要。如果有人想要,他必须亲自检查。

然而,直接循环的情况很容易检查并且会更频繁地发生,所以它是由Java完成的。

答案 4 :(得分:0)

你无法真正发现间接周期;这是暂停问题的一个典型例子。