如果收集的项目图中的某个位置是自身的引用,则对集合上的字符串可以进入无限循环。见下面的例子。
是的,良好的编码实践应该首先防止这种情况,但无论如何,我的问题是:在这种情况下检测递归的最有效方法是什么?
一种方法是在threadlocal中使用一个集合,但这看起来有点沉重。
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 !
}
}
答案 0 :(得分:11)
当我必须迭代风险图时,我通常使用递减计数器来创建函数。
例如:
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 :(得分:4)
我在问题中提到的threadlocal位:
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();
}
}
}
答案 2 :(得分:3)
问题不是集合所固有的,它可能发生在任何具有循环引用的对象图形上,例如双向链表。
我认为一个理智的策略是:如果有可能它是带有周期的对象图的一部分,则类的toString()
方法不应该调用其子项的toString()
/引用。在其他地方,我们可以有一个特殊的方法(可能是静态的,也许是一个辅助类),它产生完整图形的字符串表示。
答案 3 :(得分: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}";
}
}
答案 4 :(得分:2)
您可以始终按如下方式跟踪递归(不考虑线程问题):
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;
}
}
答案 5 :(得分:2)
我建议使用Apache Commons Lang的ToStringBuilder。在内部,它使用ThreadLocal Map来“检测循环对象引用并避免无限循环。”
答案 6 :(得分:1)
最简单的方法:永远不要在集合或地图的元素上调用toString()
。只需打印一个[]
即可表明它是一个集合或地图,并避免完全迭代它。这是避免无限递归的唯一防弹方法。
在一般情况下,您无法预测另一个对象内的Collection
或Map
中的元素,并且依赖图可能非常复杂,从而导致出现意外情况对象图中出现一个循环。
您使用的是什么IDE?因为在Eclipse中有一个选项可以在通过代码生成器生成toString()
方法时显式处理这种情况 - 这就是我使用的,当属性恰好是非空集合或地图打印[]
时它包含多少元素。
答案 7 :(得分:1)
如果你想要过分,你可以在调用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%使用语法,但我认为你明白了
答案 8 :(得分:0)
也许你可以在你的toString中创建一个Exception,并利用堆栈跟踪知道你在堆栈中的位置,你会发现它有递归调用。 有些框架就是这样做的。
@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
//...
}