25

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

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

使用 aMethodHandle或反射,我想为仅在运行时知道类型的对象找到最具体的重载方法。即我想在运行时执行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 之后执行的相同类型的解析?

4

5 回答 5

10

基本上我搜索了所有可以使用一组参数执行的方法。因此,我按照 parameterType 与 methodParameterType 之间的距离对它们进行了排序。这样做,我可以获得最具体的重载方法。

去测试:

@Test
public void test() throws Throwable {
    Object object = 1;

    Foo foo = new Foo();

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

富:

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);
    }
}

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

于 2016-10-20T04:52:58.667 回答
8

I couldn't find a way to do this with MethodHandles, but there is an interesting java.beans.Statement that implements finding the JLS' most specific method according to the Javadocs:

The execute method finds a method whose name is the same as the methodName property, and invokes the method on the target. When the target's class defines many methods with the given name the implementation should choose the most specific method using the algorithm specified in the Java Language Specification (15.11).

To retrieve the Method itself, we can do so using reflection. Here's a working example:

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");

    }
}
于 2016-10-20T21:02:47.893 回答
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。

于 2016-10-20T22:48:41.893 回答
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);
于 2016-10-20T18:51:48.237 回答
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());
    }

}
于 2016-10-26T16:14:25.693 回答