Inferentie van het reflectietype op Java 8 Lambda’s

Ik was aan het experimenteren met de nieuwe Lambda’s in Java 8, en ik ben op zoek naar een manier om reflectie op de lambda-klassen te gebruiken om het retourtype van een lambda-functie te krijgen. Ik ben vooral geïnteresseerd in gevallen waarin de lambda een generieke superinterface implementeert. In het onderstaande codevoorbeeld is MapFunction<F, T>de generieke superinterface, en ik ben op zoek naar een manier om erachter te komen welk type bindt aan de generieke parameter T.

Terwijl Java veel generieke type-informatie weggooit na de compiler, hebben subklassen (en anonieme subklassen) van generieke superklassen en generieke superinterfaces die type-informatie wel bewaard. Via reflectie werden deze types toegankelijk. In het onderstaande voorbeeld (case 1)vertelt reflectie mij dat de MyMapperimplementatie van MapFunctionbindt aan java.lang.Integernaar de generieke typeparameter T.

Zelfs voor subklassen die zelf generiek zijn, zijn er bepaalde middelen om erachter te komen wat bindt aan een generieke parameter, als er nog andere bekend zijn. Beschouw geval 2in het onderstaande voorbeeld, de IdentityMapperwaarbij zowel Fals Taan hetzelfde type binden. Als we dat weten, kennen we het type Fals we het parametertype Tkennen (wat we in mijn geval weten).

De vraag is nu, hoe kan ik iets soortgelijks realiseren voor de Java 8 lambda’s? Aangezien het eigenlijk geen reguliere subklassen zijn van de generieke superinterface, werkt de hierboven beschreven methode niet.
Kan ik er specifiek achter komen dat de parseLambdajava.lang.Integerbindt aan T, en de identityLambdabindt hetzelfde voor Fen T?

PS: In theorie zou het mogelijk moeten zijn om de lambda-code te decompileren en vervolgens een embedded compiler (zoals de JDT) te gebruiken en gebruik te maken van de type-inferentie. Ik hoop dat er een eenvoudigere manier is om dit te doen 😉

/**
 * 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;
}

Antwoord 1, autoriteit 100%

De exacte beslissing over het toewijzen van lambda-code aan interface-implementaties wordt overgelaten aan de daadwerkelijke runtime-omgeving. In principe zouden alle lambda’s die dezelfde onbewerkte interface implementeren een enkele runtime-klasse kunnen delen, net als MethodHandleProxieswel. Het gebruik van verschillende klassen voor specifieke lambda’s is een optimalisatiedie wordt uitgevoerd door de daadwerkelijke LambdaMetaFactory-implementatie, maar niet een functie die bedoeld is om foutopsporing of reflectie te ondersteunen.

Dus zelfs als u meer gedetailleerde informatie vindt in de werkelijke runtime-klasse van een lambda-interface-implementatie, zal het een artefact zijn van de momenteel gebruikte runtime-omgeving die mogelijk niet beschikbaar is in een andere implementatie of zelfs andere versies van uw huidige omgeving.

Als de lambda Serializableis, kun je het feit gebruiken dat de geserialiseerde vormbevat de methodehandtekeningvan het geïnstantieerde interfacetype om de werkelijke typevariabelewaarden bij elkaar te puzzelen.


Antwoord 2, autoriteit 79%

Dit is momenteel mogelijk op te lossen, maar alleen op een behoorlijk hackie-manier, maar laat me eerst een paar dingen uitleggen:

Als je een lambda schrijft, voegt de compiler een dynamische aanroepinstructie in die verwijst naar de LambdaMetafactoryen een eigen statische synthetische methode met de body van de lambda. De synthetische methode en de methode-handle in de constante pool bevatten beide het generieke type (als de lambda het type gebruikt of expliciet is zoals in uw voorbeelden).

Nu wordt tijdens runtime de LambdaMetaFactoryaangeroepen en wordt een klasse gegenereerd met behulp van ASM die de functionele interface en de hoofdtekst van de methode implementeert en vervolgens de statische private methode aanroept met eventuele doorgegeven argumenten. Het wordt vervolgens in de oorspronkelijke klasse geïnjecteerd met behulp van Unsafe.defineAnonymousClass(zie John Rose-bericht) zodat het toegang heeft tot de privéleden enz.

Helaas slaat de gegenereerde klasse de generieke handtekeningen niet op (het zou kunnen), dus u kunt niet de gebruikelijke reflectiemethoden gebruiken waarmee u het wissen kunt omzeilen

Voor een normale klasse zou je de bytecode kunnen inspecteren met behulp van Class.getResource(ClassName + ".class")maar voor anonieme klassen gedefinieerd met behulp van Unsafeheb je pech . U kunt echter de LambdaMetaFactoryze eruit laten dumpen met het JVM-argument:

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

Door naar het gedumpte klassenbestand te kijken (met behulp van javap -p -s -v), kan men zien dat het inderdaad de statische methode aanroept. Maar het probleem blijft hoe je de bytecode uit Java zelf kunt halen.

Dit is helaas waar het hackie wordt:

Met reflectie kunnen we Class.getConstantPoolaanroepen en vervolgens de MethodRefInfo openen om de typedescriptors te krijgen. We kunnen dan ASM gebruiken om dit te ontleden en de argumenttypes terug te geven. Alles bij elkaar:

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

Bijgewerkt met de suggestie van Jonathan

In het ideale geval zouden de klassen gegenereerd door LambdaMetaFactoryde generieke typehandtekeningen moeten opslaan (ik zou kunnen kijken of ik een patch naar de OpenJDK kan sturen), maar momenteel is dit het beste wat we kunnen doen. De bovenstaande code heeft de volgende problemen:

  • Het gebruikt ongedocumenteerde methoden en klassen
  • Het is extreem kwetsbaar voor codewijzigingen in de JDK
  • Het behoudt de generieke typen niet, dus als u List<String> in een lambda zal het uitkomen als Lijst

Antwoord 3, autoriteit 63%

Ik heb onlangs ondersteuning voor het oplossen van lambda-typeargumenten toegevoegd aan TypeTools. Bijv.:

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

De opgeloste typeargumenten zijn zoals verwacht:

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

Om een ​​doorgegeven lambda af te handelen:

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

Opmerking: de onderliggende implementatie maakt gebruik van de ConstantPool-aanpak van @danielbodart, waarvan bekend is dat deze werkt op Oracle JDK en OpenJDK (en mogelijk andere).


Antwoord 4, autoriteit 58%

Geparameteriseerde type-informatie is alleen beschikbaar tijdens runtime voor code-elementen die gebonden zijn – dat wil zeggen, specifiek gecompileerd tot een type. Lambda’s doen hetzelfde, maar aangezien je Lambda ontsuikerd is tot een methode in plaats van een type, is er geen type om die informatie vast te leggen.

Denk aan het volgende:

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

f0en f1behouden beide hun generieke type-informatie, zoals je zou verwachten. Maar aangezien het ongebonden methoden zijn die zijn gewist naar Function<Object,Object>, doen f2en f3dat niet.


Antwoord 5, autoriteit 47%

Ik heb een manier gevonden om dit te doen voor serialiseerbare lambda’s. Al mijn lambda’s zijn serialiseerbaar, dat werkt.

Bedankt, Holger, voor het wijzen op de SerializedLambda.

De generieke parameters worden vastgelegd in de synthetische statische methode van de lambda en kunnen van daaruit worden opgehaald. Het vinden van de statische methode die de lambda implementeert is mogelijk met de informatie uit de SerializedLambda

De stappen zijn als volgt:

  1. Verkrijg de SerializedLambda via de schrijfvervangingsmethode die automatisch wordt gegenereerd voor alle serialiseerbare lambda’s
  2. Zoek de klasse die de lambda-implementatie bevat (als een synthetische statische methode)
  3. Verkrijg de java.lang.reflect.Methodvoor de synthetische statische methode
  4. Haal generieke typen van die Method

UPDATE:Blijkbaar werkt dit niet met alle compilers. Ik heb het geprobeerd met de compiler van Eclipse Luna (werkt) en de Oracle javac (werkt niet).


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

Other episodes