Most efficient way to prevent an infinite recursion in toString()?

只谈情不闲聊 提交于 2019-11-29 01:26:14

When I have to iterate over risky graphs, I usually make a function with a decrementing counter.

For example :

public String toString(int dec) {
    if (  dec<=0 ) {
        return "{skipping recursion}";
    } else {
        return super.toString(dec-1);
    }
}

public String toString() {
    return toString(100);
}

I won't insist on it, as you already know it, but that doesn't respect the contract of toString() which has to be short and predictable.

The threadlocal bit I mentioned in the question:

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();
    }
}
}

You can create toString which takes an identity hash set.

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}";
   }
}
leonbloy

The problem is not inherent to collections, it can happen with any graph of objects that have cyclic references, e.g., a doubly-linked list.

I think that a sane policy is: the toString() method of your class should not call toString() of its children/referenced if there is a possibility that it's part of a object graph with cycles. Elsewhere, we could have a special methods (perhaps static, perhaps as an auxiliary class) that produces a string representation of the full graph.

You could always keep track of recursion as follows (no threading issues taken into account):

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;
   }
}

I recommend using ToStringBuilder from Apache Commons Lang. Internally it uses a ThreadLocal Map to "detect cyclical object references and avoid infinite loops."

The simplest way: don't call toString() on the elements of a collection or a map, ever. Just print a [] to indicate that it's a collection or map, and avoid iterating over it entirely. It's the only bullet-proof way to avoid falling in an infinite recursion.

In the general case, you can't anticipate what elements are going to be in a Collection or Map inside another object, and the dependency graph could be quite complex, leading to unexpected situations where a cycle occurs in the object graph.

What IDE are you using? because in Eclipse there's an option to explicitly handle this case when generating the toString() method via the code generators - that's what I use, when an attribute happens to be a non-null collection or map print [] regardless of how many elements it contains.

If you want to go overboard, you could use an aspect that tracks nested collections whenever you call 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;
    }
  }
}

I'm never 100% on syntax without throwing this into Eclipse, but I think you get the idea

maybe you could create an Exception in your toString and leverage on the stacktrace to know where you are in the stack, and you would find it there are recursive calls. Some framework does this way.

@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    
    //...    

}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!