As discussed in this question, the equals
method of java.awt.geom.Area
is defined as
public boolean equals(Area other)
instead of overriding the equals
method from Object
. That question covers the "why", and I'm interested in "how can I force Java to use the most appropriate equals
method".
Consider this example:
public static void main(String[] args) {
Class<?> cls = Area.class;
Area a1 = new Area(new Rectangle2D.Double(1, 2, 3, 4));
Area a2 = new Area(new Rectangle2D.Double(1, 2, 3, 4));
System.out.println("Areas equal: " + a1.equals(a2)); // true
Object o1 = (Object) a1;
Object o2 = (Object) a2;
System.out.println("Objects equal: " + o1.equals(o2)); // false
// Given only cls, o1, and o2, how can I get .equals() to return true?
System.out.println("cls.cast() approach : " + cls.cast(o1).equals(cls.cast(o2))); // false
try {
Method equalsMethod = cls.getMethod("equals", cls); // Exception thrown in most cases
System.out.println("Reflection approach: " + equalsMethod.invoke(o1, o2)); // true (when cls=Area.class)
} catch (Exception e) {
e.printStackTrace();
}
}
My question is: given o1
, o2
, and cls
, where o1
and o2
are guaranteed to be instances of cls
(or a subclass), how can I call the most appropriate equals
method? Assuming cls
is X.class
, I would like the following behavior:
X
defines X.equals(X)
, this is the "most appropriate" choice. (Example: X
is Area
)X
defines X.equals(Object)
, this is the second-most-appropriate choice. (Example: X
is Rectangle2D
)Object.equals(Object)
as a fallback. (Example: X
is Path2D
)In principle, I could use reflection to check for each of the above method signatures, but that seems pretty heavy-handed. Is there a simpler way?
Edit for clarity: o1
, o2
, and cls
all vary at runtime so I cannot statically cast like ((Area) o1).equals((Area) o2)
, since cls
might not be Area.class
at all times. However it is guaranteed that cls.isAssignableFrom(o1.getClass())
and cls.isAssignableFrom(o2.getClass())
are both true
.
Your second and third bullets (use X.equals(Object)
or fallback to Object.equals(Object)
) do not require any effort, as that’s what will happen anyway when calling the overridable method Object.equals(Object)
, it will use the most specific overriding method it can find.
So the only remaining task, is to invoke the X.equals(X)
method, if applicable. To minimize the associated costs, you may cache the result. Since Java 7, there is the class ClassValue
allowing to associate information with a class, in a thread safe, lazily evaluated and efficiently looked up way, still supporting garbage collection of the key class if needed.
So, a Java 7 solution may look like:
import java.lang.invoke.*;
public final class EqualsOperation extends ClassValue<MethodHandle> {
public static boolean equals(Object o, Object p) {
if(o == p) return true;
if(o == null || p == null) return false;
Class<?> t1 = o.getClass(), t2 = p.getClass();
if(t1 != t2) t1 = commonClass(t1, t2);
try {
return (boolean)OPS.get(t1).invokeExact(o, p);
} catch(RuntimeException | Error unchecked) {
throw unchecked;
} catch(Throwable ex) {
throw new IllegalStateException(ex);
}
}
private static Class<?> commonClass(Class<?> t1, Class<?> t2) {
while(t1 != Object.class && !t1.isAssignableFrom(t2)) t1 = t1.getSuperclass();
return t1;
}
static final EqualsOperation OPS = new EqualsOperation();
static final MethodHandle FALLBACK;
static {
try {
FALLBACK = MethodHandles.lookup().findVirtual(Object.class, "equals",
MethodType.methodType(boolean.class, Object.class));
} catch (ReflectiveOperationException ex) {
throw new ExceptionInInitializerError(ex);
}
}
@Override
protected MethodHandle computeValue(Class<?> type) {
try {
return MethodHandles.lookup()
.findVirtual(type, "equals", MethodType.methodType(boolean.class, type))
.asType(FALLBACK.type());
} catch(ReflectiveOperationException ex) {
return FALLBACK;
}
}
}
You may test it with
Object[] examples1 = { 100, "foo",
new Area(new Rectangle(10, 20)), new Area(new Rectangle(20, 20)) };
Object[] examples2 = { new Integer(100), new String("foo"),// enforce a!=b
new Area(new Rectangle(10, 20)) };
for(Object a: examples1) {
for(Object b: examples2) {
System.out.printf("%30s %30s: %b%n", a, b, EqualsOperation.equals(a, b));
}
}
Starting with Java 8, we can generate instances of functional interfaces at runtime, which likely improves performance, as then, we are not performing any reflective operation anymore, after encountering a type for the first time:
import java.lang.invoke.*;
import java.util.function.BiPredicate;
public final class EqualsOperation extends ClassValue<BiPredicate<Object,Object>> {
public static boolean equals(Object o, Object p) {
if(o == p) return true;
if(o == null || p == null) return false;
Class<?> t1 = o.getClass(), t2 = p.getClass();
if(t1 != t2) t1 = commonClass(t1, t2);
return OPS.get(t1).test(o, p); // test(...) is not reflective
}
private static Class<?> commonClass(Class<?> t1, Class<?> t2) {
while(t1 != Object.class && !t1.isAssignableFrom(t2)) t1 = t1.getSuperclass();
return t1;
}
static final EqualsOperation OPS = new EqualsOperation();
static final BiPredicate<Object,Object> FALLBACK = Object::equals;
@Override
protected BiPredicate<Object,Object> computeValue(Class<?> type) {
if(type == Object.class) return FALLBACK;
try {
MethodType decl = MethodType.methodType(boolean.class, type);
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(type, "equals", decl);
decl = mh.type();
BiPredicate<Object,Object> p = (BiPredicate<Object,Object>)
LambdaMetafactory.metafactory(lookup, "test",
MethodType.methodType(BiPredicate.class), decl.erase(), mh, decl)
.getTarget().invoke();
return p;
} catch(Throwable ex) {
return FALLBACK;
}
}
}
The usage is just like with the other variant.
A critical point here, is the accessibility. I assume, you only want to support public
methods declared by public
classes anyway. Still, fine tuning might be needed for Java 9+, if crossing the module border. To support custom X.equals(X)
methods declared in application code, it may need to open itself to your library for reflective access.
The problems of an equality function not matching the equality logic of other code (like collections), have been discussed in the comment at your question already. Here, similar issues as with, e.g. IdentityHashMap
, may arise; handle with care…
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With