Inferenza del tipo di riflessione su Java 8 Lambdas

Stavo sperimentando il nuovo Lambdas in Java 8 e sto cercando un modo per utilizzare la riflessione sulle classi lambda per ottenere il tipo di ritorno di una funzione lambda. Sono particolarmente interessato ai casi in cui il lambda implementa una superinterfaccia generica. Nell’esempio di codice seguente, MapFunction è la superinterfaccia generica e sto cercando un modo per scoprire quale tipo si lega al parametro generico T

Mentre Java getta via molte informazioni di tipo generico dopo il compilatore, sottoclassi (e sottoclassi anonime) di superclassi generiche e superinterfacce generiche hanno mantenuto tali informazioni di tipo. Attraverso la riflessione, questi tipi erano accessibili. Nell’esempio seguente (caso 1) , reflection dice che l’implementazione MapFunction di MapFunction lega java.lang.Integer al parametro di tipo generico T

Anche per sottoclassi che sono esse stesse generiche, ci sono determinati mezzi per scoprire cosa lega a un parametro generico, se ne conoscono altri. Considera il caso 2 nell’esempio qui sotto, IdentityMapper dove F e T legano allo stesso tipo. Quando lo sappiamo, conosciamo il tipo F se conosciamo il parametro tipo T (che nel mio caso lo facciamo).

La domanda è ora, come posso realizzare qualcosa di simile per i lambda Java 8? Poiché in realtà non sono sottoclassi regolari della superinterfaccia generica, il metodo sopra descritto non funziona. Nello specifico, posso capire che il parseLambda lega java.lang.Integer a T , e identityLambda lega lo stesso a F e T ?

PS: In teoria dovrebbe essere ansible decompilare il codice lambda e quindi usare un compilatore incorporato (come il JDT) e inserire nella sua inferenza di tipo. Spero che ci sia un modo più semplice per fare questo 😉

 /** * The superinterface. */ public interface MapFunction { T map(F value); } /** * Case 1: A non-generic subclass. */ public class MyMapper implements MapFunction { public Integer map(String value) { return Integer.valueOf(value); } } /** * A generic subclass */ public class IdentityMapper implements MapFunction { public E map(E value) { return value; } } /** * Instantiation through lambda */ public MapFunction parseLambda = (String str) -> { return Integer.valueOf(str); } public MapFunction 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; } 

La decisione esatta su come mappare il codice lambda per interfacciare le implementazioni è lasciata all’effettivo ambiente di runtime. In linea di principio, tutti i lambdas che implementano la stessa interfaccia raw potrebbero condividere una singola class runtime esattamente come fa MethodHandleProxies . L’utilizzo di classi diverse per lambda specifici è un’ottimizzazione eseguita LambdaMetafactory implementazione di LambdaMetafactory ma non una funzione destinata a facilitare il debug o il Reflection.

Pertanto, anche se si trovano informazioni più dettagliate nella class runtime effettiva di un’implementazione dell’interfaccia lambda, si tratterà di un artefatto dell’ambiente di runtime attualmente utilizzato che potrebbe non essere disponibile in diverse implementazioni o anche in altre versioni del proprio ambiente corrente.

Se il lambda è Serializable è ansible utilizzare il fatto che il modulo serializzato contiene la firma del metodo del tipo di interfaccia istanziata per risolvere insieme i valori delle variabili del tipo effettivo.

Questo è attualmente ansible risolvere, ma solo in un modo abbastanza hacker, ma vorrei prima spiegare alcune cose:

Quando scrivi un lambda, il compilatore inserisce un’istruzione di richiamo dinamico che punta al LambdaMetafactory e un metodo sintetico statico privato con il corpo del lambda. Il metodo sintetico e l’handle del metodo nel pool costante contengono entrambi il tipo generico (se lambda utilizza il tipo o è esplicito come nei tuoi esempi).

Ora in fase di esecuzione viene chiamato LambdaMetaFactory e viene generata una class utilizzando ASM che implementa l’interfaccia funzionale e il corpo del metodo chiama quindi il metodo statico privato con qualsiasi argomento passato. Viene quindi iniettato nella class originale utilizzando Unsafe.defineAnonymousClass (vedere il post di John Rose ) in modo che possa accedere ai membri privati ​​ecc.

Sfortunatamente la Classe generata non memorizza le firme generiche (potrebbe), quindi non puoi usare i soliti metodi di riflessione che ti permettono di aggirare la cancellazione

Per una class normale è ansible esaminare il codice byte utilizzando Class.getResource(ClassName + ".class") ma per le classi anonime definite utilizzando Unsafe non si è fortunati. Comunque puoi fare in modo che LambdaMetaFactory li LambdaMetaFactory uscire con l’argomento JVM:

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

Osservando il file di class scaricato (usando javap -p -s -v ), si può vedere che chiama effettivamente il metodo statico. Ma il problema rimane come ottenere il bytecode da Java stesso.

Questo purtroppo è dove ottiene hackie:

Usando reflection possiamo chiamare Class.getConstantPool e quindi accedere a MethodRefInfo per ottenere i descrittori di tipi. Possiamo quindi utilizzare ASM per analizzare questo e restituire i tipi di argomento. Mettere tutto insieme:

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

Aggiornato con il suggerimento di Jonathan

Ora idealmente le classi generate da LambdaMetaFactory dovrebbero memorizzare le firme di tipo generico (potrei vedere se posso inviare una patch a OpenJDK) ma attualmente questo è il meglio che possiamo fare. Il codice sopra riportato presenta i seguenti problemi:

  • Utilizza metodi e classi non documentati
  • È estremamente vulnerabile alle modifiche al codice nel JDK
  • Non conserva i tipi generici, quindi se passi List in un lambda verrà fuori come List

Le informazioni di tipo parametrico sono disponibili solo in fase di esecuzione per gli elementi del codice che sono vincolati, ovvero, specificatamente compilati in un tipo. Lambda fa la stessa cosa, ma siccome il tuo Lambda è de-zuccherato su un metodo piuttosto che su un tipo, non c’è alcun tipo per catturare quell’informazione.

Considera quanto segue:

 import java.util.Arrays; import java.util.function.Function; public class Erasure { static class RetainedFunction implements Function { public String apply(Integer t) { return String.valueOf(t); } } public static void main(String[] args) throws Exception { Function f0 = new RetainedFunction(); Function f1 = new Function() { public String apply(Integer t) { return String.valueOf(t); } }; Function f2 = String::valueOf; Function f3 = i -> String.valueOf(i); for (Function 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 e f1 conservano entrambe le informazioni di tipo generico, come ci si aspetterebbe. Ma siccome sono metodi non associati che sono stati cancellati in Function , f2 e f3 no.

Di recente ho aggiunto il supporto per la risoluzione degli argomenti di tipo lambda su TypeTools . Ex:

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

Gli argomenti risolti sono quelli previsti:

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

Per gestire un lambda passato:

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

Nota: l’implementazione sottostante utilizza l’approccio di ConstantPool delineato da @danielbodart, che è noto per funzionare su Oracle JDK e OpenJDK (ed eventualmente su altri).

Ho trovato un modo per farlo per i lambda serializzabili. Tutti i miei lambda sono serializzabili, a quelle opere.

Grazie, Holger, per avermi segnalato SerializedLambda .

I parametri generici vengono catturati nel metodo statico sintetico della lambda e possono essere recuperati da lì. L’individuazione del metodo statico che implementa la lambda è ansible con le informazioni di SerializedLambda

I passi sono come segue:

  1. Ottieni il SerializedLambda tramite il metodo di sostituzione della scrittura che viene generato automaticamente per tutti i lambda serializzabili
  2. Trova la class che contiene l’implementazione lambda (come metodo statico sintetico)
  3. Ottieni il metodo java.lang.reflect.Method per il metodo statico sintetico
  4. Ottieni tipi generici da quel Method

AGGIORNAMENTO: Apparentemente, questo non funziona con tutti i compilatori. L’ho provato con il compilatore di Eclipse Luna (funziona) e con l’Oracle javac (non funziona).


 // sample how to use public static interface SomeFunction extends java.io.Serializable { List applyTheFunction(Set value); } public static void main(String[] args) throws Exception { SomeFunction 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 // (the parameter, including *Double* as the generic set type) java.util.Set 

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