From 631d08d0da57013c43af77cb371d2882db1da4b3 Mon Sep 17 00:00:00 2001 From: Tobias Marstaller Date: Fri, 24 Jul 2020 16:19:37 +0200 Subject: [PATCH] Issue #302: javadocs --- .../schema/internal/EqualsCycleBreaker.java | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/everit/json/schema/internal/EqualsCycleBreaker.java b/core/src/main/java/org/everit/json/schema/internal/EqualsCycleBreaker.java index d40eac8ce..6a6fa153c 100644 --- a/core/src/main/java/org/everit/json/schema/internal/EqualsCycleBreaker.java +++ b/core/src/main/java/org/everit/json/schema/internal/EqualsCycleBreaker.java @@ -13,10 +13,77 @@ private EqualsCycleBreaker() } /** - * For each invocation of a.equals(b) that uses this class, + * ThreadLocal because this class doesn't bother with stack overflows across multiple threads, if that + * is even possible. + *
+ * A weak map so that this consumes less memory. + *
+ * For each ongoing equality check via {@link #equalsWithoutCycle(Object, Object, boolean, BiFunction)}, + * maps the this pointer of the equals invocation to all of the objects it is + * being compared against. Each mapping is removed when `equals` returns. + *
+ * This way, when {@link Object#equals(Object)} is called with the same parameters (this and the other reference) + * a second time before the first invocation has returned (= cyclic!), it can be detected and handled. */ private static final ThreadLocal> ongoingEqualityChecks = ThreadLocal.withInitial(WeakHashMap::new); + /** + * Use to break cycles in equality checks. For example: + * + *
+     *     class A {
+     *         B b;
+     *
+     *         public boolean equals(Object o) {
+     *             if (!(o instanceof A)) {
+     *                 return false;
+     *             }
+     *
+     *             return this.b.equals(((A) o).b);
+     *         }
+     *     }
+     *     class B {
+     *         int i;
+     *         A a;
+     *
+     *         public boolean equals(Object o) {
+     *             if (!(o instanceof B)) {
+     *                 return false;
+     *             }
+     *
+     *             B that = (B) o;
+     *             if (i != that.i) {
+     *                 return false;
+     *             }
+     *
+     *             return EqualsCycleBreaker.equalsWithoutCycle(this, that, true, B::equalsPossiblyCyclic);
+     *         }
+     *
+     *         private boolean equalsPossiblyCyclic(B that) {
+     *             return this.a.equals(that.a);
+     *         }
+     *     }
+     * 
+ * + * If you now construct a cyclic object tree and call equals on it, it will not explode with a stack overflow: + *
+     *     A a = new A();
+     *     B b = new B();
+     *     b.i = 10;
+     *     b.a = a;
+     *     a.b = b;
+     *
+     *     b.equals(b); // returns true
+     * 
+ * + * @param self The receiver of an invocation to {@link Object#equals(Object)}. E.g. in a.equals(b), this + * parameter is a. + * @param other The parameter of an invocation to {@link Object#equals(Object)}. E.g. in a.equals(b), this + * parameter is b. + * @param equalsOnCycle What this method should return when it detects a cycle + * @param equalityFunction The part of the equality check that can cause cyclic invocations / stack overflows. + * @return If this method is called in a cycle, returns equalsOnCycle. Otherwise defers to equalityFunction. + */ public static boolean equalsWithoutCycle(T self, T other, boolean equalsOnCycle, BiFunction equalityFunction) { Set localOngoingEqualityChecks = ongoingEqualityChecks.get() .computeIfAbsent(new Identity<>(self), (_k) -> new HashSet<>());