使用MethodHandle查找最具体的重载方法

25

假设我有一个给定类型(类/接口)中的三个方法:

public void foo(Integer integer);
public void foo(Number number);
public void foo(Object object);

我希望使用 MethodHandle 或反射,在运行时查找类型仅在运行时才知道的对象的最具体重载方法。也就是说,我想在运行时执行JLS 15.12

例如,假设我有一个包含这三个方法的上述类型的方法:

Object object = getLong(); // runtime type is Long *just an example*

MethodHandles.lookup()
             .bind(this, "foo", methodType(Void.class, object.getClass()))
             .invoke(object);

我理论上希望选择foo(Number number),但由于API仅查找foo(Long)方法而不会查找其他任何方法,所以以上代码将抛出异常。请注意,这里使用Long仅作为示例。在实际应用中,对象的类型可以是任何东西:String,MyBar,Integer等等。

MethodHandle API中是否有类似于编译器遵循JLS 15.12进行的相同解析的自动运行时解析功能?


为什么不将object.getClass()改为Number.class - Timothy Truckle
@TimothyTruckle 因为代码是通用的,不会知道任何 Number.classObject 可以是任何类型,Long 只是一个例子。 - Arjan Tijms
问题在于“重载方法”在编译时得到解决,而反射发生在运行时。因此,您需要自己找到正确的方法。也许您应该遍历对象方法并测试参数是否为object的父类... - Timothy Truckle
1
@TimothyTruckle 这就是整个问题,是否已经存在某些东西,以便我不必自己重新实现JLS 15.12。 - Arjan Tijms
2
@AR.3 这段代码片段是一个例子。一个实际的用例是,对于JSR 375,有一个CDI bean实现了IdentityStore接口,接受通用凭证。该实现可以处理任何类型的凭证。由于在此情况下重载不起作用(代理对象上只有IdentityStore接口),实现者必须一直进行向下转换。为了消除这种需求,我想在接口中提供一个默认方法来完成此操作,但当前的方法只能找到精确匹配,而不能像编译时重载(又称JLS 15.12)那样找到最佳匹配。 - Arjan Tijms
显示剩余7条评论
5个回答

10
基本上,我搜索了所有可以执行的带有一组参数的方法。因此,我按照参数类型与方法参数类型之间的距离对它们进行了排序。通过这样做,我可以获得最特定的重载方法。
测试方法:
@Test
public void test() throws Throwable {
    Object object = 1;

    Foo foo = new Foo();

    MethodExecutor.execute(foo, "foo", Void.class, object);
}

The Foo:

class Foo {
    public void foo(Integer integer) {
        System.out.println("integer");
    }

    public void foo(Number number) {
        System.out.println("number");
    }

    public void foo(Object object) {
        System.out.println("object");
    }
}

方法执行器:

public class MethodExecutor{
    private static final Map<Class<?>, Class<?>> equivalentTypeMap = new HashMap<>(18);
    static{
        equivalentTypeMap.put(boolean.class, Boolean.class);
        equivalentTypeMap.put(byte.class, Byte.class);
        equivalentTypeMap.put(char.class, Character.class);
        equivalentTypeMap.put(float.class, Float.class);
        equivalentTypeMap.put(int.class, Integer.class);
        equivalentTypeMap.put(long.class, Long.class);
        equivalentTypeMap.put(short.class, Short.class);
        equivalentTypeMap.put(double.class, Double.class);
        equivalentTypeMap.put(void.class, Void.class);
        equivalentTypeMap.put(Boolean.class, boolean.class);
        equivalentTypeMap.put(Byte.class, byte.class);
        equivalentTypeMap.put(Character.class, char.class);
        equivalentTypeMap.put(Float.class, float.class);
        equivalentTypeMap.put(Integer.class, int.class);
        equivalentTypeMap.put(Long.class, long.class);
        equivalentTypeMap.put(Short.class, short.class);
        equivalentTypeMap.put(Double.class, double.class);
        equivalentTypeMap.put(Void.class, void.class);
    }

    public static <T> T execute(Object instance, String methodName, Class<T> returnType, Object ...parameters) throws InvocationTargetException, IllegalAccessException {
        List<Method> compatiblesMethods = getCompatiblesMethods(instance, methodName, returnType, parameters);
        Method mostSpecificOverloaded = getMostSpecificOverLoaded(compatiblesMethods, parameters);
        //noinspection unchecked
        return (T) mostSpecificOverloaded.invoke(instance, parameters);
    }

    private static List<Method> getCompatiblesMethods(Object instance, String methodName, Class<?> returnType, Object[] parameters) {
        Class<?> clazz = instance.getClass();
        Method[] methods = clazz.getMethods();

        List<Method> compatiblesMethods = new ArrayList<>();

        outerFor:
        for(Method method : methods){
            if(!method.getName().equals(methodName)){
                continue;
            }

            Class<?> methodReturnType = method.getReturnType();
            if(!canBeCast(returnType, methodReturnType)){
                continue;
            }

            Class<?>[] methodParametersType = method.getParameterTypes();
            if(methodParametersType.length != parameters.length){
                continue;
            }

            for(int i = 0; i < methodParametersType.length; i++){
                if(!canBeCast(parameters[i].getClass(), methodParametersType[i])){
                    continue outerFor;
                }
            }

            compatiblesMethods.add(method);
        }

        if(compatiblesMethods.size() == 0){
            throw new IllegalArgumentException("Cannot find method.");
        }

        return compatiblesMethods;
    }

    private static Method getMostSpecificOverLoaded(List<Method> compatiblesMethods, Object[] parameters) {
        Method mostSpecificOverloaded = compatiblesMethods.get(0);
        int lastMethodScore = calculateMethodScore(mostSpecificOverloaded, parameters);

        for(int i = 1; i < compatiblesMethods.size(); i++){
            Method method = compatiblesMethods.get(i);
            int currentMethodScore = calculateMethodScore(method, parameters);
            if(lastMethodScore > currentMethodScore){
                mostSpecificOverloaded = method;
                lastMethodScore = currentMethodScore;
            }
        }

        return mostSpecificOverloaded;
    }

    private static int calculateMethodScore(Method method, Object... parameters){
        int score = 0;

        Class<?>[] methodParametersType = method.getParameterTypes();
        for(int i = 0; i < parameters.length; i++){
            Class<?> methodParameterType = methodParametersType[i];
            if(methodParameterType.isPrimitive()){
                methodParameterType = getEquivalentType(methodParameterType);
            }
            Class<?> parameterType = parameters[i].getClass();

            score += distanceBetweenClasses(parameterType, methodParameterType);
        }

        return score;
    }

    private static int distanceBetweenClasses(Class<?> clazz, Class<?> superClazz){
        return distanceFromObjectClass(clazz) - distanceFromObjectClass(superClazz);
    }

    private static int distanceFromObjectClass(Class<?> clazz){
        int distance = 0;
        while(!clazz.equals(Object.class)){
            distance++;
            clazz = clazz.getSuperclass();
        }

        return distance;
    }

    private static boolean canBeCast(Class<?> fromClass, Class<?> toClass) {
        if (canBeRawCast(fromClass, toClass)) {
            return true;
        }

        Class<?> equivalentFromClass = getEquivalentType(fromClass);
        return equivalentFromClass != null && canBeRawCast(equivalentFromClass, toClass);
    }

    private static boolean canBeRawCast(Class<?> fromClass, Class<?> toClass) {
        return fromClass.equals(toClass) || !toClass.isPrimitive() && toClass.isAssignableFrom(fromClass);
    }

    private static Class<?> getEquivalentType(Class<?> type){
        return equivalentTypeMap.get(type);
    }
}

当然,通过一些重构和注释可以改进它。

4
这绝对是非常有用的,附带代码的人们肯定会想复制你的代码。但问题实际上是关于MethodHandle API是否有类似于这样的功能而不需要有效地重新实现JLS15.12。答案可能很简单:“不,真的没有”。我主要想知道是否有标准API中做到这一点的东西我完全没有发现。 - Arjan Tijms

8

我无法使用MethodHandle找到这样做的方法,但是有一个有趣的java.beans.Statement可根据Javadocs实现查找JLS的最特定方法:

execute方法查找名称与methodName属性相同的方法,并在目标上调用该方法。当目标类定义了多个具有给定名称的方法时,实现应使用Java语言规范(15.11)中指定的算法选择最特定的方法。

要检索Method本身,我们可以使用反射。以下是一个有效的示例:

import java.beans.Statement;
import java.lang.reflect.Method;

public class ExecuteMostSpecificExample {
    public static void main(String[] args) throws Exception {
        ExecuteMostSpecificExample e = new ExecuteMostSpecificExample();
        e.process();
    }

    public void process() throws Exception {
        Object object = getLong();
        Statement s = new Statement(this, "foo", new Object[] { object });

        Method findMethod = s.getClass().getDeclaredMethod("getMethod", Class.class,
                                                           String.class, Class[].class);
        findMethod.setAccessible(true);
        Method mostSpecificMethod = (Method) findMethod.invoke(null, this.getClass(),
                                              "foo", new Class[] { object.getClass() });

        mostSpecificMethod.invoke(this, object);
    }

    private Object getLong() {
        return new Long(3L);
    }

    public void foo(Integer integer) {
        System.out.println("Integer");
    }

    public void foo(Number number) {
        System.out.println("Number");
    }

    public void foo(Object object) {
        System.out.println("Object");

    }
}

请问您下投票的原因是什么? - M A
2
对我来说看起来很正常。虽然使用MethodFinder调用Statement.getMethod方法会更清晰一些,但两者都使用内部API(例如sun.misc.Unsafe,它将被隔离)。但我猜Statement.execute是主题发起人正在寻找的东西,而且它存在于标准库中真是太棒了。谢谢! - Ivan
2
这段代码中有异味,它使用反射访问了私有静态的 getMethod 方法。但是,如果用户只想调用该方法,那么你不需要这样做,只需要在 Statement 对象上调用 execute() 方法即可。 - M A
2
如果使用 java.beans.Expression,甚至可以获得调用的结果(这是使用 Statement 无法实现的)。 - Gerald Mücke
1
@manouti,您不应该在注释中掩盖最重要的点。您应该建议首先调用“execute”,然后可能添加替代方案,以防(仅当)需要检索“Method”,但也要在答案中提到缺点,即调用不保证存在的私有API。更不用说较新版本的Java具有更强的封装性(公平地说,在您编写答案时不存在)。 - Holger

4
你可以使用MethodFinder.findMethod()来实现它。
@Test
public void test() throws Exception {
    Foo foo = new Foo();

    Object object = 3L;
    Method method = MethodFinder.findMethod(Foo.class, "foo", object.getClass());
    method.invoke(foo, object);
}


public static class Foo {
    public void foo(Integer integer) {
        System.out.println("integer");
    }

    public void foo(Number number) {
        System.out.println("number");
    }

    public void foo(Object object) {
        System.out.println("object");
    }
}

由于它位于Java根库中,因此遵循JLS 15.12。


这基本上就是我在下面回答的内容,只不过你使用了内部的 com.sun 类。这会在像 Eclipse 这样的 IDE 中显示警告。 - M A
好发现!然而,一个悲伤的事实是,这种方法并不真正标准。特别地,这将无法在Java 9中工作。所以,最好的策略是从OpenJDK的[MethodFinder](http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/774f11d707e0/src/share/classes/com/sun/beans/finder/MethodFinder.java)/[AbstractFinder](http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/774f11d707e0/src/share/classes/com/sun/beans/finder/AbstractFinder.java)中取得必要的部分(如果GPLv2条款符合您的情况)。 - apangin
很遗憾,MethodFinder不遵循JLS 15.12。特别是在某些情况下,使用null参数会抛出"java.lang.NoSuchMethodException: 找到了模棱两可的方法",而这些情况在Java编译器中被正确处理。 - Leo

3

不,我在MethodHandle API中没有看到类似的东西。在commons-beanutils中存在类似的东西,如MethodUtils#getMatchingAccessibleMethod,因此您不必自己实现。

它会看起来像这样:

Object object = getLong();
Method method = MethodUtils.getMatchingAccessibleMethod(this.getClass(), "foo", object.getClass());

你可以转换为MethodHandle API或直接使用Method:
MethodHandle handle = MethodHandles.lookup().unreflect(method);
handle.invoke(this, object);

getMatchingAccessibleMethod的注释: 该方法略微不确定,因为它循环遍历方法名称并返回第一个匹配的方法。 :D - Leo

0

考虑到以下限制:a) 参数类型只能在运行时确定,b) 只有一个参数,一个简单的解决方案是沿着类层次结构向上遍历并扫描实现的接口,就像以下示例一样。

public class FindBestMethodMatch {

    public Method bestMatch(Object obj) throws SecurityException, NoSuchMethodException {
        Class<?> superClss = obj.getClass();
        // First look for an exact match or a match in a superclass
        while(!superClss.getName().equals("java.lang.Object")) {
            try {
                return getClass().getMethod("foo", superClss);          
            } catch (NoSuchMethodException e) {
                superClss = superClss.getSuperclass();
            }
        }
        // Next look for a match in an implemented interface
        for (Class<?> intrface : obj.getClass().getInterfaces()) {
            try {
                return getClass().getMethod("foo", intrface);
            } catch (NoSuchMethodException e) { }           
        }
        // Last pick the method receiving Object as parameter if exists
        try {
            return getClass().getMethod("foo", Object.class);
        } catch (NoSuchMethodException e) { }

        throw new NoSuchMethodException("Method not found");
    }

    // Candidate methods

    public void foo(Map<String,String> map) { System.out.println("executed Map"); } 

    public void foo(Integer integer) { System.out.println("executed Integer"); } 

    public void foo(BigDecimal number) { System.out.println("executed BigDecimal"); }

    public void foo(Number number) { System.out.println("executed Number"); }

    public void foo(Object object) { System.out.println("executed Object"); }

    // Test if it works
    public static void main(String[] args) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        FindBestMethodMatch t = new FindBestMethodMatch();
        Object param = new Long(0);
        Method m = t.bestMatch(param);
        System.out.println("matched " + m.getParameterTypes()[0].getName());
        m.invoke(t, param);
        param = new HashMap<String,String>();
        m = t.bestMatch(param);
        m.invoke(t, param);
        System.out.println("matched " + m.getParameterTypes()[0].getName());
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接