防止 toString() 中无限递归的最有效方法?
Posted
技术标签:
【中文标题】防止 toString() 中无限递归的最有效方法?【英文标题】:Most efficient way to prevent an infinite recursion in toString()? 【发布时间】:2012-07-02 19:48:18 【问题描述】:如果收集项目图表中的某处是对自身的引用,则集合上的字符串可能会进入无限循环。请参阅下面的示例。
是的,良好的编码实践应该首先防止这种情况发生,但无论如何,我的问题是:在这种情况下检测递归的最有效方法是什么?
一种方法是在线程本地使用集合,但这似乎有点重。
public class AntiRecusionList<E> extends ArrayList<E>
@Override
public String toString()
if ( /* ???? test if "this" has been seen before */ )
return "skipping recursion";
else
return super.toString();
public class AntiRecusionListTest
@Test
public void testToString() throws Exception
AntiRecusionList<AntiRecusionList> list1 = new AntiRecusionList<>();
AntiRecusionList<AntiRecusionList> list2 = new AntiRecusionList<>();
list2.add(list1);
list1.add(list2);
list1.toString(); //BOOM !
【问题讨论】:
看看Lombok。它允许您使用@ToString(exclude="fields", "you","dont", "want")
注释您的类。 :)
我自己从来没有遇到过这种情况,但这样的事情可能会很痛苦,因为它只有在您打开 log4j(或类似的)调试时才会发生。因此,您尝试调试问题并最终遇到记录器消息本身的问题 - 非常烦人! +1 回答我的问题
@davidfrancis - 日志和调试正是出现的地方!干杯。
【参考方案1】:
当我必须遍历有风险的图时,我通常会创建一个带有递减计数器的函数。
例如:
public String toString(int dec)
if ( dec<=0 )
return "skipping recursion";
else
return super.toString(dec-1);
public String toString()
return toString(100);
我不会坚持,正如你已经知道的那样,但这不尊重toString()
的合同,该合同必须简短且可预测。
【讨论】:
+1 表示这个想法,尽管这仅在对象引用其自己类型的另一个对象的情况下才有效,但在大多数情况下,我们有类似人员属于有很多人的部门(创建一个循环),在这种情况下没有解决方案 当图是异构的时候,通常可以做一个带递减计数器的静态递归函数。或同一方法的多个专业化。重要的一点是通过计数器并在它降至零时失败。 简单优雅,拯救了我的一天【参考方案2】:我在问题中提到的线程本地位:
public class AntiRecusionList<E> extends ArrayList<E>
private final ThreadLocal<IdentityHashMap<AntiRecusionList<E>, ?>> fToStringChecker =
new ThreadLocal<IdentityHashMap<AntiRecusionList<E>, ?>>()
@Override
protected IdentityHashMap<AntiRecusionList<E>, ?> initialValue()
return new IdentityHashMap<>();
;
@Override
public String toString()
boolean entry = fToStringChecker.get().size() == 0;
try
if (fToStringChecker.get().containsKey(this)/* test if "this" has been seen before */)
return "skipping recursion";
else
fToStringChecker.get().put(this, null);
entry = true;
return super.toString();
finally
if (entry)
fToStringChecker.get().clear();
【讨论】:
这是一个安全的赌注,涉及的开销最小。【参考方案3】:您可以创建带有身份哈希集的 toString。
public String toString()
return toString(Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>()));
private String toString(Set<Object> seen)
if (seen.add(this))
// to string this
else
return "this";
【讨论】:
这不会在图表中每次遇到 AntiRecusionList 类型时创建一个新集合吗?因此永远不会产生预期的效果,因为 if (seen) 测试将始终针对新集合? 虽然我很喜欢你如何设置身份。我会偷那个:)【参考方案4】:我推荐使用 Apache Commons Lang 的 ToStringBuilder。在内部,它使用 ThreadLocal Map 来“检测循环对象引用并避免无限循环”。
【讨论】:
【参考方案5】:这个问题不是集合所固有的,它可能发生在任何具有循环引用的对象图上,例如双向链表。
我认为一个理智的策略是:如果您的类的 toString()
方法有可能是具有循环的对象图的一部分,则不应调用其子级/引用的 toString()
。在其他地方,我们可以有一个特殊的方法(也许是静态的,也许作为一个辅助类)来生成完整图的字符串表示。
【讨论】:
【参考方案6】:您始终可以按如下方式跟踪递归(不考虑线程问题):
public static class AntiRecusionList<E> extends ArrayList<E>
private boolean recursion = false;
@Override
public String toString()
if(recursion)
//Recursion's base case. Just return immediatelly with an empty string
return "";
recursion = true;//start a perhaps recursive call
String result = super.toString();
recursion = false;//recursive call ended
return result;
【讨论】:
...我认为他是对的,不是吗?我找不到这个想法的缺陷,它很有效。而且这个线程定位很容易。 @Slanec 问题是它不会在之前看到一个对象时停止,而是在一级递归后停止。它只会阻止递归,不会在集合中找到循环。 @DavidCowden: 列表的toString
只是在要显示的列表的每个项目上调用toString
。停止递归会达到预期的效果,因为对象已经被打印。这并不是 OP 试图完成的真正的图形算法,至少在我理解这里的问题的方式上
好吧,也许我理解错了。无论哪种方式,OP都要求一种方法来打印集合中的所有元素,并且仅在找到循环时才停止。不,他不是在寻找循环,但这只是表达他的意图的简洁方式。
好的,这适用于 ArrayList,您可以在不使用递归的情况下迭代整个集合。如果你不能使用链接列表呢?那么你就有可能没有把所有的元素都打印出来。【参考方案7】:
最简单的方法:永远不要在集合或地图的元素上调用toString()
。只需打印一个[]
以表明它是一个集合或地图,并避免完全迭代它。这是避免陷入无限递归的唯一防弹方法。
在一般情况下,您无法预测另一个对象中的 Collection
或 Map
中会包含哪些元素,并且依赖关系图可能非常复杂,从而导致出现循环的意外情况对象图。
您使用的是什么 IDE?因为在 Eclipse 中有一个选项可以在通过代码生成器生成 toString()
方法时显式处理这种情况 - 这就是我使用的,当属性恰好是非空集合或映射打印 []
而不管有多少元素它包含。
【讨论】:
哈希图为什么不能防弹? @Aidanc 这是同样的情况,对于一般情况,您无法预测哈希图将在哪里使用,它可能很容易在对象图中的循环中间结束和繁荣! - 无限递归。【参考方案8】:如果你想做得过火,你可以在调用 toString() 时使用跟踪嵌套集合的方面。
public aspect ToStringTracker()
Stack collections = new Stack();
around( java.util.Collection c ): call(String java.util.Collection+.toString()) && target(c)
if (collections.contains(c)) return "recursion";
else
collections.push(c);
String r = c.toString();
collections.pop();
return r;
如果不把它扔到 Eclipse 中,我永远不会 100% 了解语法,但我想你明白了
【讨论】:
【参考方案9】:也许您可以在您的 toString 中创建一个异常并利用堆栈跟踪来了解您在堆栈中的位置,并且您会发现它存在递归调用。 一些框架就是这样做的。
@Override
public String toString()
// ...
Exception exception = new Exception();
StackTraceElement[] stackTrace = exception.getStackTrace();
// now you analyze the array: stack trace elements have
// 4 properties: check className, lineNumber and methodName.
// if analyzing the array you find recursion you stop propagating the calls
// and your stack won't explode
//...
【讨论】:
以上是关于防止 toString() 中无限递归的最有效方法?的主要内容,如果未能解决你的问题,请参考以下文章