Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java reflection to call overloaded method Area.equals(Area)

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:

  • If X defines X.equals(X), this is the "most appropriate" choice. (Example: X is Area)
  • Otherwise, if X defines X.equals(Object), this is the second-most-appropriate choice. (Example: X is Rectangle2D)
  • If neither of the above are true, I want to call 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.

like image 523
k_ssb Avatar asked Apr 19 '18 10:04

k_ssb


1 Answers

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…

like image 97
Holger Avatar answered Oct 31 '22 14:10

Holger