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 IdentityMapper
où F
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;
}
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:
Java.lang.reflect.Method
pour la méthode statique synthétiqueMethod
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");
}
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 .
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:
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.
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).