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

Posted

技术标签:

【中文标题】使用 MethodHandle 查找最具体的重载方法【英文标题】:Finding most specific overloaded method using MethodHandle 【发布时间】:2017-02-26 08:35:53 【问题描述】:

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

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 @TimothyTruckle 因为代码是通用的,不知道任何Number.classObject 可以是任何类型,Long 只是一个示例。 这里的问题是“重载方法”在编译时得到解决,而反射发生在运行时。因此,您必须自己找到合适的方法。也许您应该遍历 objects 方法并测试参数是否是 object 的 Parent ... @TimothyTruckle 这几乎是整个问题,是否已经存在某些东西,这样我就不必自己重新实现 JLS 15.12。 @AR.3 sn-p 就是一个例子。一个实际用例是,对于 JSR 375,有一个 CDI bean 实现了采用通用凭据的 IdentityStore 接口。该实现可以处理任何类型的凭据。由于在这种情况下重载不起作用(代理对象上只有 IdentityStore 接口),因此实现者必须一直向下转换。为了消除这种需要,我想在执行此操作的接口中提供一个默认方法,但当前的方法只能找到完全匹配,而不是编译时重载(又名 JLS 15.12)那样的最佳匹配。 【参考方案1】:

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

测试:

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

    Foo foo = new Foo();

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

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

MethodExecutor:

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

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

【讨论】:

这绝对是非常有用的,登陆这个问题的人肯定会想复制你的代码,但问题实际上是关于 MethodHandle API 中是否有东西可以做类似的事情 没有有效地重新实现 JLS 15.12。答案可能很简单:“不,真的没有”。我主要想知道标准 API 中是否没有我完全缺少的东西。【参考方案2】:

我找不到使用MethodHandles 的方法,但是有一个有趣的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");

    

【讨论】:

我可以知道投反对票的原因吗? 不知道,对我来说看起来是合法的。尽管使用MethodFinder 调用的Statement. getMethod 方法会更干净一些,并且两者都使用内部API(例如sun.misc.Unsafe,它将被隔离)。但我猜Statement.execute 是主题启动者正在寻找的东西,它存在于标准库中是令人惊奇的事情。谢谢 这段代码的味道是通过反射访问私有静态getMethod,但如果用户only打算调用该方法,则不需要这样做,只需要在Statement对象上调用execute()即可。 如果使用java.beans.Expression,你甚至可以获得调用的结果(Statement 不能) @manouti 你不应该把最重要的一点埋在评论里。您应该建议首先调用execute,然后可能会在(且仅当)需要检索Method时添加替代方案,但在答案中也提到缺点,即调用私有API不保证在那里。更不用说对较新 Java 版本的更强封装(公平地说,当您编写答案时,它并不存在)。【参考方案3】:

你可以使用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 中显示警告。 好发现!然而,可悲的是,这种方法并不是真正的标准。特别是,这在 Java 9 中不起作用。因此,最好的策略是从 OpenJDK 中获取 MethodFinder/AbstractFinder 的基本部分(如果 GPLv2 条款适合您的情况)。 不幸的是 MethodFinder 不遵循 JLS 15.12。特别是“java.lang.NoSuchMethodException: Ambiguous methods are found”在某些情况下会抛出空参数,而这些情况由 Java 编译器正确处理。【参考方案4】:

不,我在 MethodHandle API 中没有看到类似的东西。 commons-beanutilsMethodUtils#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【参考方案5】:

考虑到以下约束: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());
    


【讨论】:

以上是关于使用 MethodHandle 查找最具体的重载方法的主要内容,如果未能解决你的问题,请参考以下文章

Gradle Android 错误:MethodHandle.invoke 和 MethodHandle.invokeExact

方法重载和选择最具体的类型

错误:仅从 Android O 开始支持 MethodHandle.invoke 和 MethodHandle.invokeExact (--min-api 26)

Android studio“MethodHandle.invoke 和 MethodHandle.invokeExact 仅支持从 Android O (--min-api 26) 开始”构建 AP

请简述重载和重写的区别

封装继承和多态,重写重载等基础复习