Самый эффективный способ предотвратить бесконечную рекурсию в toString()?
В строку в коллекции можно попасть в бесконечный цикл, если где-то на графике собранных элементов будет ссылка на себя. См. Пример ниже.
Да, хорошие методы кодирования должны помешать этому, в первую очередь, но в любом случае, мой вопрос: какой самый эффективный способ обнаружить рекурсию в этой ситуации?
Один из подходов - использовать набор в 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 !
}
}
Ответы
Ответ 1
Я уже упоминал в вопросе вопрос:
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
Когда мне нужно перебирать рискованные графики, я обычно делаю функцию с декрементирующим счетчиком.
Например:
public String toString(int dec) {
if ( dec<=0 ) {
return "{skipping recursion}";
} else {
return super.toString(dec-1);
}
}
public String toString() {
return toString(100);
}
Я не буду настаивать на этом, как вы уже знаете, но это не соответствует контракту toString()
, который должен быть коротким и предсказуемым.
Ответ 3
Проблема не присуща коллекциям, это может произойти с любым графиком объектов, которые имеют циклические ссылки, например, двусвязный список.
Я думаю, что разумная политика: метод toString()
вашего класса не должен вызывать toString()
своих дочерних элементов/ссылок, если есть вероятность, что это часть графика объекта с циклами. В другом месте у нас могут быть специальные методы (возможно, статические, возможно, как вспомогательный класс), которые создают строковое представление полного графика.
Ответ 4
Вы можете создать 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}";
}
}
Ответ 5
Вы всегда можете отслеживать рекурсию следующим образом (без учета потоков):
public static class AntiRecusionList<E> extends ArrayList<E> {
private boolean recursion = false;
@Override
public String toString() {
if(recursion){
//Recursion 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;
}
}
Ответ 6
Я рекомендую использовать ToStringBuilder из Apache Commons Lang. Внутри он использует карту ThreadLocal для "обнаружения циклических ссылок на объекты и исключения бесконечных циклов".
Ответ 7
Самый простой способ: не вызывать toString()
на элементах коллекции или карты, когда-либо. Просто напечатайте []
, чтобы указать, что это коллекция или карта, и избегайте повторного ее завершения. Это единственный пуленепробиваемый способ избежать падения в бесконечной рекурсии.
В общем случае вы не можете предвидеть, какие элементы будут находиться в Collection
или Map
внутри другого объекта, а граф зависимостей может быть довольно сложным, что приводит к непредвиденным ситуациям, в которых происходит цикл графа объектов.
Какую IDE вы используете? потому что в Eclipse есть возможность явно обрабатывать этот случай при генерации метода toString()
через генераторы кода - то, что я использую, когда атрибут является ненулевой или картографической печатью []
независимо от того, сколько элементов он содержит.
Ответ 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;
}
}
}
Я никогда не на 100% синтаксис, не бросая это в Eclipse, но я думаю, что вы поняли идею
Ответ 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
//...
}