防止 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()。只需打印一个[] 以表明它是一个集合或地图,并避免完全迭代它。这是避免陷入无限递归的唯一防弹方法。

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

您使用的是什么 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() 中无限递归的最有效方法?的主要内容,如果未能解决你的问题,请参考以下文章

左程云-递归和动态规划

递归与无限极分类

PHP二叉树递归遍历无限循环问题

防止重放攻击最有效的方法是

方法结束前的递归不是无限循环?

运用递归解决二叉树相关问题