17

如果收集项目图表中的某处是对自身的引用,则对集合进行字符串处理可能会陷入无限循环。请参见下面的示例。

是的,良好的编码实践应该首先防止这种情况,但无论如何,我的问题是:在这种情况下检测递归的最有效方法是什么?

一种方法是在 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 !
  }
}
4

9 回答 9

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()必须简短且可预测的合同。

于 2012-07-02T19:52:29.540 回答
5

我在问题中提到的线程本地位:

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();
    }
}
}
于 2012-07-02T20:02:19.123 回答
3

我推荐使用 Apache Commons Lang 的 ToStringBuilder。在内部,它使用 ThreadLocal Map 来“检测循环对象引用并避免无限循环”。

于 2013-04-05T15:56:23.517 回答
3

这个问题不是集合所固有的,它可能发生在任何具有循环引用的对象图上,例如双向链表。

我认为一个理智的政策是:如果您的类的方法有可能是带有循环的对象图的一部分,则toString()不应调用其子类/引用的方法。toString()在其他地方,我们可以有一个特殊的方法(可能是静态的,可能作为辅助类)来生成完整图的字符串表示。

于 2012-07-02T20:00:41.743 回答
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}";
   }
}
于 2012-07-02T20:04:02.213 回答
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;
   }
}
于 2012-07-02T20:21:11.620 回答
1

最简单的方法:永远不要调用toString()集合或地图的元素。只需打印 a[]以表明它是一个集合或地图,并避免完全迭代它。这是避免陷入无限递归的唯一防弹方法。

在一般情况下,您无法预测哪些元素将位于另一个对象中CollectionMap内部,并且依赖关系图可能非常复杂,从而导致对象图中出现循环的意外情况。

你用的是什么IDE?因为在 Eclipse 中有一个选项可以在通过代码生成器生成方法时显式处理这种情况toString()- 这就是我使用的,当属性恰好是非空集合或映射打印[]时,无论它包含多少元素。

于 2012-07-02T19:50:55.597 回答
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% 了解语法,但我想你明白了

于 2012-07-02T20:50:37.737 回答
0

也许您可以在您的 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    
    //...    

}
于 2012-07-02T20:02:54.790 回答