web-dev-qa-db-fra.com

Inférence de type de réflexion sur Java 8 Lambdas

J'expérimentais avec les nouvelles Lambdas en Java 8, et je cherche un moyen d'utiliser la réflexion sur les classes lambda pour obtenir le type de retour d'une fonction lambda. Je suis particulièrement intéressé par les cas où le lambda implémente une superinterface générique. Dans l'exemple de code ci-dessous, MapFunction<F, T> est la superinterface générique, et je cherche un moyen de savoir quel type se lie au paramètre générique T.

Alors que Java jette beaucoup d'informations de type générique après le compilateur, les sous-classes (et les sous-classes anonymes) de superclasses génériques et de superinterfaces génériques ont préservé ces informations de type. Par réflexion, ces types étaient accessibles. Dans l'exemple ci-dessous (cas 1), la réflexion me dit que l'implémentation MyMapper de MapFunction lie Java.lang.Integer au paramètre de type générique T.

Même pour les sous-classes qui sont elles-mêmes génériques, il existe certains moyens de savoir ce qui se lie à un paramètre générique, si d'autres sont connus. Considérez cas 2 dans l'exemple ci-dessous, le IdentityMapperF et T se lient au même type. Quand nous savons cela, nous connaissons le type F si nous connaissons le type de paramètre T (ce que dans mon cas nous connaissons).

La question est maintenant, comment puis-je réaliser quelque chose de similaire pour les Java 8 lambdas? Puisqu'ils ne sont en fait pas des sous-classes régulières de la superinterface générique, la méthode décrite ci-dessus ne fonctionne pas. Spécifiquement, puis-je comprendre que parseLambda lie Java.lang.Integer à T, et le identityLambda le lie à F et T?

PS: En théorie, il devrait être possible de décompiler le code lambda, puis d'utiliser un compilateur intégré (comme le JDT) et d'exploiter son inférence de type. J'espère qu'il existe un moyen plus simple de le faire ;-)

/**
 * The superinterface.
 */
public interface MapFunction<F, T> {

    T map(F value);
}

/**
 * Case 1: A non-generic subclass.
 */
public class MyMapper implements MapFunction<String, Integer> {

    public Integer map(String value) {
        return Integer.valueOf(value);
    }
}

/**
 * A generic subclass
 */
public class IdentityMapper<E> implements MapFunction<E, E> {

    public E map(E value) {
        return value;
    }

}

/**
 * Instantiation through lambda
 */

public MapFunction<String, Integer> parseLambda = (String str) -> { return Integer.valueOf(str); }

public MapFunction<E, E> identityLambda = (value) -> { return value; }


public static void main(String[] args)
{
    // case 1
    getReturnType(MyMapper.class);    // -> returns Java.lang.Integer

    // case 2
    getReturnTypeRelativeToParameter(IdentityMapper.class, String.class);    // -> returns Java.lang.String
}

private static Class<?> getReturnType(Class<?> implementingClass)
{
    Type superType = implementingClass.getGenericInterfaces()[0];

    if (superType instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) superType;
        return (Class<?>) parameterizedType.getActualTypeArguments()[1];
    }
    else return null;
}

private static Class<?> getReturnTypeRelativeToParameter(Class<?> implementingClass, Class<?> parameterType)
{
    Type superType = implementingClass.getGenericInterfaces()[0];

    if (superType instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) superType;
        TypeVariable<?> inputType = (TypeVariable<?>) parameterizedType.getActualTypeArguments()[0];
        TypeVariable<?> returnType = (TypeVariable<?>) parameterizedType.getActualTypeArguments()[1];

        if (inputType.getName().equals(returnType.getName())) {
            return parameterType;
        }
        else {
            // some logic that figures out composed return types
        }
    }

    return null;
}
48
Stephan Ewen

J'ai trouvé un moyen de le faire pour les lambdas sérialisables. Tous mes lambdas sont sérialisables, ça marche.

Merci, Holger, de m'avoir indiqué le SerializedLambda.

Les paramètres génériques sont capturés dans la méthode statique synthétique du lambda et peuvent être récupérés à partir de là. Trouver la méthode statique qui implémente le lambda est possible avec les informations du SerializedLambda

Les étapes sont les suivantes:

  1. Obtenez le SerializedLambda via la méthode de remplacement d'écriture qui est générée automatiquement pour tous les lambdas sérialisables
  2. Recherchez la classe qui contient l'implémentation lambda (en tant que méthode statique synthétique)
  3. Obtenir le Java.lang.reflect.Method pour la méthode statique synthétique
  4. Obtenez des types génériques à partir de ce Method

PDATE: Apparemment, cela ne fonctionne pas avec tous les compilateurs. Je l'ai essayé avec le compilateur d'Eclipse Luna (fonctionne) et Oracle javac (ne fonctionne pas).


// sample how to use
public static interface SomeFunction<I, O> extends Java.io.Serializable {

    List<O> applyTheFunction(Set<I> value);
}

public static void main(String[] args) throws Exception {

    SomeFunction<Double, Long> lambda = (set) -> Collections.singletonList(set.iterator().next().longValue());

    SerializedLambda sl = getSerializedLambda(lambda);      
    Method m = getLambdaMethod(sl);

    System.out.println(m);
    System.out.println(m.getGenericReturnType());
    for (Type t : m.getGenericParameterTypes()) {
        System.out.println(t);
    }

    // prints the following
    // (the method) private static Java.util.List test.ClassWithLambdas.lambda$0(Java.util.Set)
    // (the return type, including *Long* as the generic list type) Java.util.List<Java.lang.Long>
    // (the parameter, including *Double* as the generic set type) Java.util.Set<Java.lang.Double>

// getting the SerializedLambda
public static SerializedLambda getSerializedLambda(Object function) {
    if (function == null || !(function instanceof Java.io.Serializable)) {
        throw new IllegalArgumentException();
    }

    for (Class<?> clazz = function.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
        try {
            Method replaceMethod = clazz.getDeclaredMethod("writeReplace");
            replaceMethod.setAccessible(true);
            Object serializedForm = replaceMethod.invoke(function);

            if (serializedForm instanceof SerializedLambda) {
                return (SerializedLambda) serializedForm;
            }
        }
        catch (NoSuchMethodError e) {
            // fall through the loop and try the next class
        }
        catch (Throwable t) {
            throw new RuntimeException("Error while extracting serialized lambda", t);
        }
    }

    throw new Exception("writeReplace method not found");
}

// getting the synthetic static lambda method
public static Method getLambdaMethod(SerializedLambda lambda) throws Exception {
    String implClassName = lambda.getImplClass().replace('/', '.');
    Class<?> implClass = Class.forName(implClassName);

    String lambdaName = lambda.getImplMethodName();

    for (Method m : implClass.getDeclaredMethods()) {
        if (m.getName().equals(lambdaName)) {
            return m;
        }
    }

    throw new Exception("Lambda Method not found");
}
7
Stephan Ewen

La décision exacte de mapper le code lambda aux implémentations d'interface est laissée à l'environnement d'exécution réel. En principe, tous les lambdas implémentant la même interface brute pourraient partager une seule classe d'exécution comme le fait MethodHandleProxies . L'utilisation de différentes classes pour des lambdas spécifiques est une optimisation effectuée par l'implémentation réelle de LambdaMetafactory mais pas une fonctionnalité destinée à faciliter le débogage ou la réflexion.

Ainsi, même si vous trouvez des informations plus détaillées dans la classe d'exécution réelle d'une implémentation d'interface lambda, ce sera un artefact de l'environnement d'exécution actuellement utilisé qui pourrait ne pas être disponible dans différentes implémentations ou même dans d'autres versions de votre environnement actuel.

Si le lambda est Serializable, vous pouvez utiliser le fait que le forme sérialisée contient le signature de la méthode du type d'interface instancié pour énoncer ensemble les valeurs réelles des variables de type .

17
Holger

Il est actuellement possible de résoudre ce problème, mais uniquement de manière assez hackie, mais permettez-moi d'abord d'expliquer quelques choses:

Lorsque vous écrivez un lambda, le compilateur insère une instruction d'invocation dynamique pointant vers LambdaMetafactory et une méthode synthétique statique privée avec le corps du lambda. La méthode synthétique et le handle de méthode dans le pool constant contiennent tous les deux le type générique (si le lambda utilise le type ou est explicite comme dans vos exemples).

Désormais, lors de l'exécution, LambdaMetaFactory est appelé et une classe est générée à l'aide d'ASM qui implémente l'interface fonctionnelle et le corps de la méthode appelle ensuite la méthode statique privée avec tous les arguments passés. Il est ensuite injecté dans la classe d'origine en utilisant Unsafe.defineAnonymousClass (Voir John Rose post ) afin qu'il puisse accéder aux membres privés, etc.

Malheureusement, la classe générée ne stocke pas les signatures génériques (elle pourrait), vous ne pouvez donc pas utiliser les méthodes de réflexion habituelles qui vous permettent de contourner l'effacement

Pour une classe normale, vous pouvez inspecter le bytecode à l'aide de Class.getResource(ClassName + ".class") mais pour les classes anonymes définies à l'aide de Unsafe, vous n'avez pas de chance. Cependant vous pouvez faire les LambdaMetaFactory les vider avec l'argument JVM:

Java -Djdk.internal.lambda.dumpProxyClasses=/some/folder

En regardant le fichier de classe vidé (en utilisant javap -p -s -v), On peut voir qu'il appelle bien la méthode statique. Mais le problème reste de savoir comment obtenir le bytecode de l'intérieur Java lui-même.

C'est malheureusement là que ça devient hackie:

En utilisant la réflexion, nous pouvons appeler Class.getConstantPool Puis accéder à MethodRefInfo pour obtenir les descripteurs de type. Nous pouvons ensuite utiliser ASM pour analyser cela et renvoyer les types d'arguments. Mettre tous ensemble:

Method getConstantPool = Class.class.getDeclaredMethod("getConstantPool");
getConstantPool.setAccessible(true);
ConstantPool constantPool = (ConstantPool) getConstantPool.invoke(lambda.getClass());
String[] methodRefInfo = constantPool.getMemberRefInfoAt(constantPool.size() - 2);

int argumentIndex = 0;
String argumentType = jdk.internal.org.objectweb.asm.Type.getArgumentTypes(methodRef[2])[argumentIndex].getClassName();
Class<?> type = (Class<?>) Class.forName(argumentType);

Mis à jour avec la suggestion de Jonathan

Maintenant, idéalement, les classes générées par LambdaMetaFactory devraient stocker les signatures de type génériques (je pourrais voir si je peux soumettre un correctif à OpenJDK) mais actuellement c'est le mieux que nous puissions faire. Le code ci-dessus présente les problèmes suivants:

  • Il utilise des méthodes et des classes non documentées
  • Il est extrêmement vulnérable aux changements de code dans le JDK
  • Il ne conserve pas les types génériques, donc si vous passez List <String> dans un lambda, il apparaîtra sous la forme List

Les informations de type paramétrées ne sont disponibles au moment de l'exécution que pour les éléments de code liés, c'est-à-dire spécifiquement compilés dans un type. Les Lambdas font la même chose, mais comme votre Lambda est désucrée à une méthode plutôt qu'à un type, il n'y a pas de type pour capturer ces informations.

Considérer ce qui suit:

import Java.util.Arrays;
import Java.util.function.Function;

public class Erasure {

    static class RetainedFunction implements Function<Integer,String> {
        public String apply(Integer t) {
            return String.valueOf(t);
        }
    }

    public static void main(String[] args) throws Exception {
        Function<Integer,String> f0 = new RetainedFunction();
        Function<Integer,String> f1 = new Function<Integer,String>() {
            public String apply(Integer t) {
                return String.valueOf(t);
            }
        };
        Function<Integer,String> f2 = String::valueOf;
        Function<Integer,String> f3 = i -> String.valueOf(i);

        for (Function<Integer,String> f : Arrays.asList(f0, f1, f2, f3)) {
            try {
                System.out.println(f.getClass().getMethod("apply", Integer.class).toString());
            } catch (NoSuchMethodException e) {
                System.out.println(f.getClass().getMethod("apply", Object.class).toString());
            }
            System.out.println(Arrays.toString(f.getClass().getGenericInterfaces()));
        }
    }
}

f0 et f1 les deux conservent leurs informations de type générique, comme vous vous en doutez. Mais comme ce sont des méthodes non liées qui ont été effacées en Function<Object,Object>, f2 et f3 ne pas.

11
MrPotes

J'ai récemment ajouté la prise en charge de la résolution des arguments de type lambda à TypeTools . Ex:

MapFunction<String, Integer> fn = str -> Integer.valueOf(str);
Class<?>[] typeArgs = TypeResolver.resolveRawArguments(MapFunction.class, fn.getClass());

Les arguments de type résolus sont comme prévu:

assert typeArgs[0] == String.class;
assert typeArgs[1] == Integer.class;

Pour gérer un lambda passé:

public void call(Callable<?> c) {
  // Assumes c is a lambda
  Class<?> callableType = TypeResolver.resolveRawArguments(Callable.class, c.getClass());
}

Remarque: L'implémentation sous-jacente utilise l'approche ConstantPool décrite par @danielbodart qui est connue pour fonctionner sur Oracle JDK et OpenJDK (et peut-être d'autres).

11
Jonathan