用于调用重载方法的Java反射Area.equals(Area)

pkp*_*pnd 6 java reflection equals

正如在这个问题中讨论的那样,equals方法java.awt.geom.Area被定义为

public boolean equals(Area other)

而不是重写equals方法Object.这个问题涵盖了"为什么",我对"如何强制Java使用最合适的equals方法" 感兴趣.

考虑这个例子:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我的问题是:给定o1,o2cls,o1以及o2保证是cls(或子类)的实例,我如何调用最合适的equals方法?假设clsX.class,我想要以下行为:

  • 如果X定义X.equals(X),这是"最合适"的选择.(例如:XArea)
  • 否则,如果X定义X.equals(Object),这是第二个最合适的选择.(例如:XRectangle2D)
  • 如果以上都不是真的,我想称之为Object.equals(Object)后备.(例如:XPath2D)

原则上,我可以使用反射来检查上述每个方法签名,但这看起来非常严厉.有更简单的方法吗?

为了清晰起见编辑:o1,o2并且cls所有都在运行时变化,所以我不能静态地投射((Area) o1).equals((Area) o2),因为cls可能不是Area.class在任何时候.但是可以保证cls.isAssignableFrom(o1.getClass()),并cls.isAssignableFrom(o2.getClass())true.

Hol*_*ger 1

您的第二个和第三个项目符号(使用X.equals(Object)或回退到Object.equals(Object))不需要任何努力,因为 \xe2\x80\x99s 在调用可重写方法时无论如何都会发生什么Object.equals(Object),它将使用它可以找到的最具体的重写方法。

\n\n

因此,唯一剩下的任务是调用该X.equals(X)方法(如果适用)。为了最大限度地减少相关成本,您可以缓存结果。从Java\xc2\xa07开始,有一个类ClassValue允许以线程安全、延迟评估和有效查找的方式将信息与类关联起来,如果需要,仍然支持关键类的垃圾收集。

\n\n

因此,Java\xc2\xa07 解决方案可能如下所示:

\n\n
import java.lang.invoke.*;\n\npublic final class EqualsOperation extends ClassValue<MethodHandle> {\n    public static boolean equals(Object o, Object p) {\n        if(o == p) return true;\n        if(o == null || p == null) return false;\n        Class<?> t1 = o.getClass(), t2 = p.getClass();\n        if(t1 != t2) t1 = commonClass(t1, t2);\n        try {\n            return (boolean)OPS.get(t1).invokeExact(o, p);\n        } catch(RuntimeException | Error unchecked) {\n            throw unchecked;\n        } catch(Throwable ex) {\n            throw new IllegalStateException(ex);\n        }\n    }\n    private static Class<?> commonClass(Class<?> t1, Class<?> t2) {\n        while(t1 != Object.class && !t1.isAssignableFrom(t2)) t1 = t1.getSuperclass();\n        return t1;\n    }\n    static final EqualsOperation OPS = new EqualsOperation();\n    static final MethodHandle FALLBACK;\n    static {\n        try {\n            FALLBACK = MethodHandles.lookup().findVirtual(Object.class, "equals",\n                MethodType.methodType(boolean.class, Object.class));\n        } catch (ReflectiveOperationException ex) {\n            throw new ExceptionInInitializerError(ex);\n        }\n    }\n\n    @Override\n    protected MethodHandle computeValue(Class<?> type) {\n        try {\n            return MethodHandles.lookup()\n                .findVirtual(type, "equals", MethodType.methodType(boolean.class, type))\n                .asType(FALLBACK.type());\n        } catch(ReflectiveOperationException ex) {\n            return FALLBACK;\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

你可以测试它

\n\n
Object[] examples1 = { 100, "foo",\n    new Area(new Rectangle(10, 20)), new Area(new Rectangle(20, 20)) };\nObject[] examples2 = { new Integer(100), new String("foo"),// enforce a!=b\n   new Area(new Rectangle(10, 20)) };\nfor(Object a: examples1) {\n    for(Object b: examples2) {\n        System.out.printf("%30s %30s: %b%n", a, b, EqualsOperation.equals(a, b));\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

从 Java\xc2\xa08 开始,我们可以在运行时生成函数接口的实例,这可能会提高性能,因为在第一次遇到类型后,我们不再执行任何反射操作:

\n\n
import java.lang.invoke.*;\nimport java.util.function.BiPredicate;\n\npublic final class EqualsOperation extends ClassValue<BiPredicate<Object,Object>> {\n    public static boolean equals(Object o, Object p) {\n        if(o == p) return true;\n        if(o == null || p == null) return false;\n        Class<?> t1 = o.getClass(), t2 = p.getClass();\n        if(t1 != t2) t1 = commonClass(t1, t2);\n        return OPS.get(t1).test(o, p); // test(...) is not reflective\n    }\n    private static Class<?> commonClass(Class<?> t1, Class<?> t2) {\n        while(t1 != Object.class && !t1.isAssignableFrom(t2)) t1 = t1.getSuperclass();\n        return t1;\n    }\n    static final EqualsOperation OPS = new EqualsOperation();\n    static final BiPredicate<Object,Object> FALLBACK = Object::equals;\n\n    @Override\n    protected BiPredicate<Object,Object> computeValue(Class<?> type) {\n        if(type == Object.class) return FALLBACK;\n        try {\n            MethodType decl = MethodType.methodType(boolean.class, type);\n            MethodHandles.Lookup lookup = MethodHandles.lookup();\n            MethodHandle mh = lookup.findVirtual(type, "equals", decl);\n            decl = mh.type();\n            BiPredicate<Object,Object> p = (BiPredicate<Object,Object>)\n                LambdaMetafactory.metafactory(lookup, "test",\n                    MethodType.methodType(BiPredicate.class), decl.erase(), mh, decl)\n                .getTarget().invoke();\n            return p;\n        } catch(Throwable ex) {\n            return FALLBACK;\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

用法与其他变体一样。

\n\n

这里的一个关键点是可访问性。我假设,您只想支持类public声明的方法public。尽管如此,如果跨越模块边界,Java\xc2\xa09+ 可能需要进行微调。为了支持X.equals(X)应用程序代码中声明的自定义方法,它可能需要向您的库开放以进行反射访问。

\n\n

相等函数与其他代码(如集合)的相等逻辑不匹配的问题已经在您的问题的评论中讨论过。在这里,可能会出现与例如 类似的问题IdentityHashMap;小心搬运\xe2\x80\xa6

\n