Inferencia de tipo de reflexión en Java 8 Lambdas


Estaba experimentando con las nuevas Lambdas en Java 8, y estoy buscando una manera de usar la reflexión en las clases lambda para obtener el tipo de retorno de una función lambda. Estoy especialmente interesado en los casos en los que la lambda implementa una superinterface genérica. En el siguiente ejemplo de código, MapFunction<F, T> es la superinterface genérica, y estoy buscando una manera de averiguar qué tipo se une al parámetro genérico T.

Mientras que Java arroja una gran cantidad de información de tipo genérico después de la compilador, subclases (y subclases anónimas) de superclases genéricas y superinterfaces genéricas preservaron esa información de tipo. A través de la reflexión, estos tipos eran accesibles. En el ejemplo siguiente (caso 1), la reflexión me dice que la implementación MyMapper de MapFunction se une a java.lang.Integer con el parámetro de tipo genérico T.

Incluso para las subclases que son genéricas, hay ciertos medios para averiguar qué se une a un parámetro genérico, si se conocen algunos otros. Considere el caso 2 en el ejemplo siguiente, el IdentityMapper donde ambos F y T se unen al mismo tipo. Cuando sabemos eso, conocemos el tipo F si conocemos el parámetro type T (que en mi caso sí conocemos).

La pregunta es ahora, ¿cómo puedo realizar algo similar para el Java 8 lambdas? Dado que en realidad no son subclases regulares de la superinterface genérica, el método descrito anteriormente no funciona. Específicamente, puedo averiguar que el parseLambda se une java.lang.Integer a T, y el identityLambda une lo mismo a F y T?

PD: En teoría debería ser posible descompilar el código lambda y luego usar un compilador embebido (como el JDT) y aprovechar su inferencia de tipos. Espero que haya una manera más sencilla de hacer esto; -)

/**
 * 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;
}
Author: Paul Bellora, 2014-02-19

5 answers

La decisión exacta de cómo asignar el código lambda a las implementaciones de la interfaz se deja al entorno de tiempo de ejecución real. En principio, todos los lambdas que implementan la misma interfaz raw podrían compartir una sola clase de tiempo de ejecución como MethodHandleProxies lo hace. El uso de diferentes clases para lambdas específicas es una optimización realizada por la implementación real LambdaMetafactory, pero no una característica destinada a ayudar a la depuración o Reflexión.

Así que incluso si usted encuentra información más detallada en el real clase de tiempo de ejecución de una implementación de interfaz lambda será un artefacto del entorno de tiempo de ejecución utilizado actualmente que podría no estar disponible en una implementación diferente o incluso en otras versiones de su entorno actual.

Si la lambda es Serializable puede usar el hecho de que la forma serializada contiene la firma del método del tipo de interfaz instanciado para unir los valores de la variable de tipo real.

 15
Author: Holger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-11 18:27:18

Esto es actualmente posible de resolver, pero solo de una manera bastante hackie, pero permítanme primero explicar algunas cosas:

Cuando se escribe una lambda, el compilador inserta una instrucción dynamic invoke apuntando a LambdaMetafactory y un método sintético estático privado con el cuerpo de la lambda. El método sintético y el identificador del método en el grupo de constantes contienen el tipo genérico (si lambda usa el tipo o es explícito como en sus ejemplos).

Ahora en runtime se llama a LambdaMetaFactory y se genera una clase usando ASM que implementa la interfaz funcional y el cuerpo del método luego llama al método estático privado con cualquier argumento pasado. Luego se inyecta en la clase original usando Unsafe.defineAnonymousClass (ver John Rose post) para que pueda acceder a los miembros privados, etc.

Desafortunadamente la Clase generada no almacena las firmas genéricas (podría), por lo que no puede usar los métodos de reflexión habituales que le permiten moverse borrado

Para una Clase normal puedes inspeccionar el bytecode usando Class.getResource(ClassName + ".class") pero para las clases anónimas definidas usando Unsafe no tienes suerte. Sin embargo, puedes hacer que LambdaMetaFactory los vuelque con el argumento JVM:

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

Al mirar el archivo de clase volcado (usando javap -p -s -v), uno puede ver que efectivamente llama al método estático. Pero el problema sigue siendo cómo obtener el bytecode desde dentro de Java.

Por desgracia, aquí es donde se pone hackie:

Usando reflexión podemos llamar a Class.getConstantPool y luego acceder al MethodRefInfo para obtener los descriptores de tipo. Entonces podemos usar ASM para analizar esto y devolver los tipos de argumento. Poniendo todo junto:

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

Actualizado con la sugerencia de jonathan

Ahora idealmente las clases generadas por LambdaMetaFactory deberían almacenar las firmas de tipo genéricas (podría ver si puedo enviar un parche al OpenJDK) pero actualmente esto es lo mejor que podemos hacer. El código anterior tiene la problemas siguientes:

  • Utiliza métodos y clases no documentados
  • Es extremadamente vulnerable a los cambios de código en el JDK
  • No conserva los tipos genéricos, por lo que si pasa List a una lambda, aparecerá como List
 12
Author: Daniel Worthington-Bodart,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-09 15:34:53

La información de tipo parametrizado solo está disponible en tiempo de ejecución para los elementos de código que están enlazados, es decir, compilados específicamente en un tipo. Lambdas hace lo mismo, pero como su Lambda está des-azucarado a un método en lugar de a un tipo, no hay ningún tipo para capturar esa información.

Considere lo siguiente:

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 y f1 ambos conservan su información de tipo genérico, como es de esperar. Pero como son métodos no vinculados que se han borrado a Function<Object,Object>, f2 y f3 no.

 10
Author: MrPotes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-03 17:17:22

Recientemente he añadido soporte para resolver argumentos de tipo lambda a TypeTools. Ex:

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

Los args de tipo resuelto son los esperados:

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

Para manejar una lambda pasada:

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

Nota: La implementación subyacente utiliza el enfoque de ConstantPool descrito por @danielbodart que se sabe que funciona en Oracle JDK y OpenJDK (y posiblemente otros).

 7
Author: Jonathan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-18 17:51:06

He encontrado una manera de hacerlo para lambdas serializables. Todas mis lambdas son serializables, para que funcione.

Gracias, Holger, por señalarme al SerializedLambda.

Los parámetros genéricos se capturan en el método estático sintético de lambda y se pueden recuperar desde allí. Encontrar el método estático que implementa la lambda es posible con la información de la SerializedLambda

Los pasos son los siguientes:

  1. Obtenga el SerializedLambda a través del reemplazo de escritura método que se genera automáticamente para todas las lambdas serializables
  2. Encuentre la clase que contiene la implementación lambda (como un método estático sintético)
  3. Obtenga el java.lang.reflect.Method para el método estático sintético
  4. Obtener tipos genéricos de que Method

UPDATE: Aparentemente, esto no funciona con todos los compiladores. Lo he probado con el compilador de Eclipse Luna (funciona) y el Oracle javac (no lo hace trabajo).


// 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");
}
 5
Author: Stephan Ewen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-08 18:01:10