17

To string on a collection can get into a infinite loop if somewhere in the graph of collected items is a reference back to itself. See example below.

Yes, good coding practices should prevent this in the first place, but anyway, my question is: What is the most efficient way to detect a recursion in this situation?

One approach is to use a set in a threadlocal, but that seems a bit heavy.

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 !
  }
}
marathon
  • 7,881
  • 17
  • 74
  • 137
  • 4
    Take a look at [Lombok](http://projectlombok.org/). It allows you to annotate your classes with `@ToString(exclude={"fields", "you","dont", "want"})`. :) – Elias Dorneles Jul 02 '12 at 19:53
  • 1
    Never run into this myself but things like this can be a REAL pain , because it could only occur when you switch your log4j (or similar) debugging on. So you're trying to debug a problem and end up running into an issue with the logger messages themselves - very annoying! +1 for question from me – davidfrancis Jul 02 '12 at 20:04
  • @davidfrancis - logging and debugging is exactly where this came up! Cheers. – marathon Jul 02 '12 at 20:10

9 Answers9

11

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.

Denys Séguret
  • 372,613
  • 87
  • 782
  • 758
  • 1
    +1 for the idea, although this will work just in case the object reference another object of its own type, in majority of cases we have something like person belongs to department that has many persons (creating a cycle), and on this situation there is no solution – Francisco Spaeth Jul 02 '12 at 20:06
  • When the graph is heterogeneous, you can usually make a static recursive function with a decrementing counter. Or multiple specializations of the same method. The important point is to pass the counter and to fail when it's down to zero. – Denys Séguret Jul 03 '12 at 05:57
  • simple and elegant and saved my day – Leo Feb 12 '14 at 17:12
5

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();
    }
}
}
marathon
  • 7,881
  • 17
  • 74
  • 137
3

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.

emallove
  • 1,417
  • 1
  • 17
  • 22
leonbloy
  • 73,180
  • 20
  • 142
  • 190
3

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}";
   }
}
Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • won't this create a new set every time a AntiRecusionList type is encountered in the graph? And thus never have desired effect because the if (seen) test will always be against a new set? – marathon Jul 02 '12 at 20:08
  • 3
    though I do like how you made the identity set. I shall steal that :) – marathon Jul 02 '12 at 20:37
3

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

Patrick Crocker
  • 193
  • 2
  • 6
2

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;
   }
}
Cratylus
  • 52,998
  • 69
  • 209
  • 339
  • ...I think he's right, isn't he? I can't find a flaw in the idea, it simply works. And the thread-locationing of this is easy. – Petr Janeček Jul 02 '12 at 20:54
  • @Slanec the problem is it doesn't stop when an object has been seen before, rather, it just stops after one level of recursion. It will only prevent recursion, not find a cycle in a collection. – dcow Jul 02 '12 at 20:57
  • @DavidCowden:the `toString` of the list just calls the `toString` on each item of the list to be presented for display.Stoping the recursion does what is expected since the object will already have been printed. This is not really a graph algorithm that the OP is trying to accomplish, at least in the way I understand the issue here – Cratylus Jul 02 '12 at 21:06
  • Well maybe I am understanding it wrong. Either way, the OP asked for a way to print all elements in a collection and stop only if a cycle is found. No he's not looking for a cycle, but that's just a concise way of saying what his intention is. – dcow Jul 02 '12 at 21:08
  • Okay, so that works for an ArrayList where you can iterate over the whole collection without using recursion. What about a linked list where you can't? Then you risk not printing all the elements out. – dcow Jul 02 '12 at 21:14
  • After a good night sleep, I still think this works pretty much as well as the other solutions. If this instance of the `List` is a part of the cycle, it will be discovered and taken care of at the first possible moment. I can't see the `LinkedList` problem David Cowden mentioned earlier =/. – Petr Janeček Jul 03 '12 at 10:55
  • @DavidCowden:I am sorry.I don't get what you are asking here – Cratylus Jul 03 '12 at 17:06
  • I was thinking, "what if you can't iterate over all the elements in your collection without recursion". However, after sleeping on it too, I think I was combining `toString()` with the actual recursive traversal of a collection. If you can visit each element of your collection, and call `toString()` using the above implementation, I think it works. So as far as I can tell now, this would work. – dcow Jul 03 '12 at 19:37
1

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.

Óscar López
  • 232,561
  • 37
  • 312
  • 386
  • Why wouldn't a hashmap be bullet proof? – Aidanc Jul 02 '12 at 19:51
  • 2
    @Aidanc it's the same situation, for the general case you can't anticipate where the hashmap is going to be used, it could just as easily end up in the middle of a cycle in the object graph and boom! - infinite recursion. – Óscar López Jul 02 '12 at 19:56
1

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

Jochen
  • 2,277
  • 15
  • 22
0

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

}
JayZee
  • 851
  • 1
  • 10
  • 17