25

Suppose I have three methods inside a given type (class/interface):

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

Using a MethodHandle or reflection, I'd like to find the most specific overloaded method for an object of which the type is only known at runtime. i.e. I'd like to do JLS 15.12 at runtime.

For instance, suppose I have the following in a method of the type mentioned above that contains those three methods:

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

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

Then I conceptually would want foo(Number number) to be chosen, but the above will throw an exception since the API will only look for a foo(Long) method and nothing else. Note that the usage of Long here is just as an example. The type of the object could be anything in practice; String, MyBar, Integer, ..., etc., etc.

Is there something in the MethodHandle API that automatically and at runtime does the same kind of resolution that the compiler does following JLS 15.12?

Arjan Tijms
  • 37,782
  • 12
  • 108
  • 140
  • why don't you change `object.getClass()` to `Number.class`? – Timothy Truckle Oct 17 '16 at 11:27
  • @TimothyTruckle because the code is general and wouldn't know about any `Number.class`. `Object` can be of any type, `Long` is just an example. – Arjan Tijms Oct 17 '16 at 11:41
  • The problem here is that "overloaded methods" get resolved at compile time while reflection happens at runtime. Therfore you're on your own to find the proper method. Maybe you should iterate over the objects method and test if the argument is an Parent of the `object`... – Timothy Truckle Oct 17 '16 at 11:51
  • 1
    @TimothyTruckle That's pretty much the entire question, whether something already exists so that I don't have to reimplement JLS 15.12 myself. – Arjan Tijms Oct 17 '16 at 12:06
  • What is the purpose of retrieving the method handle for the most specific method in your application? Is it just to call it as your snippet shows? – M A Oct 20 '16 at 07:23
  • 2
    @AR.3 the snippet is an example. A practical use case is that for JSR 375 there's a CDI bean that implements the IdentityStore interface taking a general Credential. The implementation can handle any type of Credential. Since overloading would not work in this case (there's only the IdentityStore interface on the proxy object), the implementor would have to downcast all the time. To remove this need I wanted to provide a default method in the interface that does this, but the current one only finds exact matches, not a best matching one as compile time overloads (aka JLS 15.12) do. – Arjan Tijms Oct 20 '16 at 17:20
  • The JLS 15.12 is probably the most complicated part of the JLS, and I think that it is **far** more complicated than it looks. The current answer by Paulo is a nice try, but I'm pretty sure that it only covers the most simple cases, and is thus not a "strict" implementation of JLS 15.12. Implementing this properly is **really, really** hard. In my attempts, I did not even *consider* to support varargs (they are the holy grail). You can guess how hard it is when you search for [eclipse+ambiguous+method+bug](http://stackoverflow.com/search?q=eclipse+ambiguous+method+bug) here on stackoverflow – Marco13 Oct 20 '16 at 19:09
  • ... BUT ... you probably already knew all that ;-) Just wondering: Would the answer: "No, this is not possible with `MethodHandle`" **really** be worth the 500 bounty for you? – Marco13 Oct 20 '16 at 19:10
  • @ArjanTijms, why not make the `IdentityStore` interface generic, to get the same functional behaviour without reflection (implementors declare the type they handle), or use a specific annotation on the method(s) to invoke? Do you expect many implementations will support several types? – Didier L Oct 23 '16 at 17:12
  • @DidierL I (we actually) have been considering generics, but the idea was indeed to have implementations support several types. Possibly we could abandon that feature. We're still left then with the current setup where the using code of the `IdentityStore` puts a credential type in the method and if the implementation doesn't handle it a status code is returned that says so. With a generic that part wouldn't work and a separate test method would be needed (which can be automated again, since the structural generic parameter can be read via reflection). Choices choices ;) – Arjan Tijms Oct 24 '16 at 21:44
  • @Marco13 "Implementing this properly is really, really hard." - Indeed, and that's not even taking into consideration the maintenance costs of updating such code when newer versions of Java come out for which JLS 15.12 is updated. And yes, if the "No this is not possible" is truly authoritative and/or well enough supported it's easily worth the bounty ;) I have both spend weeks for things that at long last really didn't appear to be possible, but also the other way around that I thought something wasn't possible while there was a method in the standard lib all along. – Arjan Tijms Oct 24 '16 at 21:48
  • One could *try* to stand "on the shoulders of giants": Eclipse is open source, and eventually boils down to a bunch of JARs held together by the RCP structure. It might be possible to figure out the relevant JARs - and there *must* be one where this "most specific method" computation is implemented! - and just use it. But I'm not sure how feasible this would be in practice for this particular case. (I once considered this for a similarly complicated (and somewhat related!) task, namely the *type inference*, but only really *used* the Eclipse JARs for some AST parsing until now) – Marco13 Oct 25 '16 at 01:40

5 Answers5

10

Basically I searched all methods that can be executed with a set of parameters. So, I sorted them by the distance between the parameterType to the methodParameterType. Doing this, I could get the most specific overloaded method.

To test:

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

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

Ofcourse it can be improved with some refactoring and comments.

Paulo
  • 2,956
  • 3
  • 20
  • 30
  • 4
    This is absolutely incredibly useful and people landing on this question will surely want to copy your code, but the question was actually about whether there was something in the MethodHandle API that could do something like that *without effectively re-implementing JLS 15.12*. The answer could be a simple: "no, there really isn't". I was mostly wondering if there isn't something in the standard API that does this that I'm totally missing. – Arjan Tijms Oct 20 '16 at 07:20
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");

    }
}
M A
  • 71,713
  • 13
  • 134
  • 174
  • May I know the reason for downvoting? – M A Oct 21 '16 at 06:05
  • 2
    Dunno, looks legit for me. Although using `MethodFinder` that `Statement. getMethod` method calls out to would be a bit cleaner and both are using internal API's (like `sun.misc.Unsafe` which is going to be insulated). But I guess `Statement.execute` is what the topic starter is looking for and it's amazing thing that it exists in std lib. Thanks – Ivan Oct 21 '16 at 07:34
  • 2
    What smells in this code is accessing the private static `getMethod` with reflection, but if the user _only_ intends to invoke the method, you don't need to do that, only need to call `execute()` on the `Statement` object. – M A Oct 21 '16 at 13:49
  • 2
    if using `java.beans.Expression` you could even get the result of the invocation (which you can't with `Statement`) – Gerald Mücke Oct 26 '16 at 10:06
  • 1
    @manouti you should not bury the most important point in a comment. You should suggest calling `execute` in the first place and might then add the alternative for the case if (and only if) the `Method` needs to be retrieved but mention the drawbacks also right in the answer, i.e. calling a private API that is not guaranteed to be there. Not to speak of the stronger encapsulation of newer Java versions (which, to be fair, didn’t exist when you wrote the answer). – Holger Oct 28 '19 at 11:46
4

You can use MethodFinder.findMethod() to achieve it.

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

Since it is in java root library, it is following JLS 15.12.

Paulo
  • 2,956
  • 3
  • 20
  • 30
  • This is basically what I answered below, except that you're using the internal `com.sun` class. This will show a warning in IDEs like Eclipse. – M A Oct 21 '16 at 07:29
  • Good finding! However, a sad thing is that this method is not really standard. In particular, this won't work in Java 9. So, the best strategy would be to take the essential part of [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) from OpenJDK (if GPLv2 terms fit your case). – apangin Oct 21 '16 at 22:28
  • Unfortunately MethodFinder does not follow JLS 15.12. In particular "java.lang.NoSuchMethodException: Ambiguous methods are found" is thrown in some cases with null arguments, while those cases are handled correctly by the Java compiler. – Leo Oct 22 '18 at 15:41
3

No, I haven't seen anything like that in MethodHandle API. Similar thing exists in commons-beanutils as MethodUtils#getMatchingAccessibleMethod so you don't have to implement that.

It will look something like this:

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

You can convert to MethodHandle API or just use the Method directly:

MethodHandle handle = MethodHandles.lookup().unreflect(method);
handle.invoke(this, object);
Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
Ivan
  • 3,781
  • 16
  • 20
  • From a comment to `getMatchingAccessibleMethod`: This method is slightly undeterministic since it loops through methods names and return the first matching method. :D – Leo Sep 20 '18 at 09:26
0

Given the constraints that: a) the type of the parameter is only known at runtime, and b) there is only one parameter, a simple solution can be just walking up the class hierarchy and scanning the implemented interfaces like in the following example.

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

}
Serg M Ten
  • 5,568
  • 4
  • 25
  • 48