web-dev-qa-db-fra.com

Comment obtenir la MethodInfo d'une référence de méthode Java 8?

S'il vous plaît jeter un oeil sur le code suivant:

Method methodInfo = MyClass.class.getMethod("myMethod");

Cela fonctionne, mais le nom de la méthode est transmis sous forme de chaîne. Ainsi, cela compilera même si myMethod n'existe pas.

Par ailleurs, Java 8 introduit une fonctionnalité de référence de méthode. Il est vérifié au moment de la compilation. Il est possible d’utiliser cette fonctionnalité pour obtenir des informations sur la méthode?

printMethodName(MyClass::myMethod);

Exemple complet:

@FunctionalInterface
private interface Action {

    void invoke();
}

private static class MyClass {

    public static void myMethod() {
    }
}

private static void printMethodName(Action action) {
}

public static void main(String[] args) throws NoSuchMethodException {
    // This works, but method name is passed as a string, so this will compile
    // even if myMethod does not exist
    Method methodInfo = MyClass.class.getMethod("myMethod");

    // Here we pass reference to a method. It is somehow possible to
    // obtain Java.lang.reflect.Method for myMethod inside printMethodName?
    printMethodName(MyClass::myMethod);
}

En d'autres termes, j'aimerais avoir un code qui soit l'équivalent du code C # suivant:

    private static class InnerClass
    {
        public static void MyMethod()
        {
            Console.WriteLine("Hello");
        }
    }

    static void PrintMethodName(Action action)
    {
        // Can I get Java.lang.reflect.Method in the same way?
        MethodInfo methodInfo = action.GetMethodInfo();
    }

    static void Main()
    {
        PrintMethodName(InnerClass.MyMethod);
    }
58
Rafal

Non, il n'y a pas de moyen fiable et supporté de le faire. Vous affectez une référence de méthode à une instance d'une interface fonctionnelle, mais cette instance est créée par LambdaMetaFactory et il n'existe aucun moyen de l'explorer pour trouver la méthode à laquelle vous êtes lié à l'origine.

Les Lambdas et les références de méthodes en Java fonctionnent assez différemment des délégués en C #. Pour des informations intéressantes, lisez invokedynamic.

D'autres réponses et commentaires montrent qu'il est actuellement possible de récupérer la méthode liée avec un travail supplémentaire, mais assurez-vous de bien comprendre les mises en garde.

22
Mike Strobel

Cela semble être possible après tout, en utilisant un proxy pour enregistrer quelle méthode est appelée.

https://stackoverflow.com/a/22745127/3478229

14
ddan

Dans mon cas, je cherchais un moyen de m'en débarrasser lors de tests unitaires:

Point p = getAPoint();
assertEquals(p.getX(), 4, "x");
assertEquals(p.getY(), 6, "x");

Comme vous pouvez le voir, une personne teste la méthode getAPoint et vérifie que les coordonnées sont conformes aux attentes, mais la description de chaque assertion a été copiée et ne correspond pas à ce qui est vérifié. Mieux serait d'écrire ceci une seule fois.

A partir des idées de @ddan, j'ai construit une solution proxy en utilisant Mockito:

private<T> void assertPropertyEqual(final T object, final Function<T, ?> getter, final Object expected) {
    final String methodName = getMethodName(object.getClass(), getter);
    assertEquals(getter.apply(object), expected, methodName);
}

@SuppressWarnings("unchecked")
private<T> String getMethodName(final Class<?> clazz, final Function<T, ?> getter) {
    final Method[] method = new Method[1];
    getter.apply((T)Mockito.mock(clazz, Mockito.withSettings().invocationListeners(methodInvocationReport -> {
        method[0] = ((InvocationOnMock) methodInvocationReport.getInvocation()).getMethod();
    })));
    return method[0].getName();
}

Non, je peux simplement utiliser

assertPropertyEqual(p, Point::getX, 4);
assertPropertyEqual(p, Point::getY, 6);

et il est garanti que la description de l'assertion est en phase avec le code.

Inconvénient:

  • Sera légèrement plus lent que ci-dessus
  • A besoin de Mockito pour travailler
  • À peine utile à autre chose que le cas d'utilisation ci-dessus.

Cependant, cela montre comment cela pourrait être fait.

7
yankee

Bien que je n’aie pas essayé moi-même, je pense que la réponse est "non", car une référence de méthode est sémantiquement identique à un lambda.

5
Matt Ball

Si vous pouvez rendre l'interface Action extend Serializable, alors cette réponse d'une autre question semble fournir une solution (au moins sur certains compilateurs et certains environnements d'exécution).

3
user102008

Il peut ne pas y avoir de moyen fiable, mais dans certaines circonstances:

  1. votre MyClass n'est pas final et a un constructeur accessible (limitation de cglib)
  2. votre myMethod n'est pas surchargé et non statique

Vous pouvez essayer d'utiliser cglib pour créer un proxy de MyClass, puis d'utiliser un MethodInterceptor pour signaler le Method pendant que la référence à la méthode est appelée dans un prochain essai.

Exemple de code:

public static void main(String[] args) {
    Method m = MethodReferenceUtils.getReferencedMethod(ArrayList.class, ArrayList::contains);
    System.out.println(m);
}

Vous verrez la sortie suivante:

public boolean Java.util.ArrayList.contains(Java.lang.Object)

Tandis que:

public class MethodReferenceUtils {

    @FunctionalInterface
    public static interface MethodRefWith1Arg<T, A1> {
        void call(T t, A1 a1);
    }

    public static <T, A1> Method getReferencedMethod(Class<T> clazz, MethodRefWith1Arg<T, A1> methodRef) {
        return findReferencedMethod(clazz, t -> methodRef.call(t, null));
    }

    @SuppressWarnings("unchecked")
    private static <T> Method findReferencedMethod(Class<T> clazz, Consumer<T> invoker) {
        AtomicReference<Method> ref = new AtomicReference<>();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                ref.set(method);
                return null;
            }
        });
        try {
            invoker.accept((T) enhancer.create());
        } catch (ClassCastException e) {
            throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", clazz));
        }

        Method method = ref.get();
        if (method == null) {
            throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", clazz));
        }

        return method;
    }
}

Dans le code ci-dessus, MethodRefWith1Arg est simplement un sucre de syntaxe pour vous permettre de référencer une méthode non statique avec un argument. Vous pouvez créer autant que MethodRefWithXArgs pour référencer vos autres méthodes.

1
vivimice

Vous pouvez ajouter safety-mirror à votre chemin de classe et procéder comme ceci:

Method m1 = Types.createMethod(Thread::isAlive)  // Get final method
Method m2 = Types.createMethod(String::isEmpty); // Get method from final class
Method m3 = Types.createMethod(BufferedReader::readLine); // Get method that throws checked exception
Method m4 = Types.<String, Class[]>createMethod(getClass()::getDeclaredMethod); //to get vararg method you must specify parameters in generics
Method m5 = Types.<String>createMethod(Class::forName); // to get overloaded method you must specify parameters in generics
Method m6 = Types.createMethod(this::toString); //Works with inherited methods

La bibliothèque propose également une méthode getName(...):

assertEquals("isEmpty", Types.getName(String::isEmpty));

La bibliothèque est basée sur la réponse de Holger: https://stackoverflow.com/a/21879031/6095334

Edit: La bibliothèque a plusieurs défauts dont je prends lentement conscience ... .. Voir le commentaire de fx Holger ici: Comment obtenir le nom de la méthode résultant d'un lambda

1
Hervian

Vous pouvez utiliser ma bibliothèque Reflect Without String

Method myMethod = ReflectWithoutString.methodGetter(MyClass.class).getMethod(MyClass::myMethod);
0
Dean Xu