2016-10-17 5 views
23

Предположим, что у меня есть три метода внутри данного типа (класс/интерфейс):Нахождение наиболее конкретный перегруженный метод с использованием MethodHandle

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, ... и т. Д. И т. Д.

Есть ли что-то в API MethodHandle, который автоматически и во время выполнения делает то же разрешение, что и компилятор после JLS 15.12?

+0

Почему вы не меняете 'object.getClass()' на 'Number.class'? –

+0

@TimothyTruckle, потому что код является общим и не знает ни о каком 'Number.class'. «Объект» может быть любого типа, «Длинный» - просто пример. –

+0

Проблема заключается в том, что «перегруженные методы» решаются во время компиляции, а отражение происходит во время выполнения. Поэтому вы сами можете найти правильный метод. Может быть, вам нужно перебрать метод объектов и проверить, является ли аргумент родителем объекта? ... –

ответ

8

В основном я искал все методы, которые могут быть выполнены с помощью набора параметров. Итак, я отсортировал их на расстоянии между параметромType до методаParameterType. Делая это, я мог бы получить наиболее специфический перегруженный метод.

Для теста:

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

Ofcourse он может B улучшено с некоторыми рефакторингом и комментариями.

+2

Это абсолютно невероятно полезно, и люди, приземляющиеся на этот вопрос обязательно захочет скопировать ваш код, но на самом деле возник вопрос о том, есть ли что-то в API MethodHandle, который мог бы сделать что-то подобное * без эффективной повторной реализации JLS 15.12 *. Ответ может быть простым: «нет, на самом деле нет». Я в основном задавался вопросом, не есть ли что-то в стандартном API, который делает это, что я полностью отсутствует. –

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

Я не мог найти способ сделать это с MethodHandle с, но есть интересный java.beans.Statement, который реализует поиск самого специфического метода JLS в соответствии с Javadocs:

Метод 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"); 

    } 
} 
+0

Могу ли я узнать причину падения? – manouti

+2

Dunno, выглядит законно для меня. Хотя использование 'MethodFinder',' 'Statement. метод getMethod' будет немного чище, и оба используют внутренний API (например, 'sun.misc.Unsafe', который будет изолирован). Но я думаю, что 'Statement.execute' - это то, что ищет стартер темы, и удивительно, что он существует в std lib. Спасибо – Ivan

+2

Что пахнет этим кодом, он обращается к частному статическому 'getMethod' с отражением, но если пользователь _only_ намеревается вызывать метод, вам не нужно это делать, нужно только вызвать' execute() 'в' Объект Statement'. – manouti

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.

+1

Это довольно круто, спасибо – Ivan

+0

Это в основном то, что я ответил ниже, используя внутренний класс 'com.sun'. Это покажет предупреждение в IDE, например Eclipse. – manouti

+0

Хорошая работа! Однако печально, что этот метод не является стандартным. В частности, это не будет работать в Java 9. Итак, лучшей стратегией было бы взятие существенной части [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/ class/com/sun/beans/finder/AbstractFinder.java) из OpenJDK (если условия GPLv2 соответствуют вашему делу). – apangin

0

С учетом ограничений, которые: а) типа параметра известен только во время выполнения и б) есть только один параметр, простое решение может быть просто ходьба вверх по иерархии классов и сканирование реализовано интерфейсы, как в следующем примере.

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

}