Вывод типа отражения на Java 8 Lambdas - программирование
Подтвердить что ты не робот

Вывод типа отражения на Java 8 Lambdas

Я экспериментировал с новым Lambdas в Java 8, и я ищу способ использовать отражение в классах лямбда, чтобы получить возвращаемый тип лямбда-функции. Меня особенно интересуют случаи, когда лямбда реализует общий суперинтерфейс. В приведенном ниже примере кода MapFunction<F, T> является общим суперинтерфейсом, и я ищу способ узнать, какой тип привязан к общему параметру T.

В то время как Java сбрасывает много общей информации типа после компилятора, подклассы (и анонимные подклассы) общих суперклассов и общих суперинтерфейсов сохраняли эту информацию типа. Благодаря отражению эти типы были доступны. В приведенном ниже примере (случай 1) отражение говорит мне, что реализация MyMapper MapFunction связывает java.lang.Integer с параметром типового типа T.

Даже для подклассов, которые сами по себе являются универсальными, существуют определенные способы выяснить, что связывается с общим параметром, если известны некоторые другие. Рассмотрим пример 2 в приведенном ниже примере, IdentityMapper, где оба F и T привязаны к одному типу. Когда мы это знаем, мы знаем тип F, если мы знаем тип параметра T (что в моем случае мы делаем).

Вопрос в том, как я могу реализовать что-то подобное для Java 8 lambdas? Поскольку они на самом деле не являются регулярными подклассами общего суперинтерфейса, описанный выше метод не работает. В частности, могу ли я понять, что parseLambda связывает java.lang.Integer с T, а identityLambda связывает то же самое с F и T?

PS: Теоретически можно декомпилировать лямбда-код, а затем использовать встроенный компилятор (например, JDT) и коснуться его вывода типа. Я надеюсь, что есть более простой способ сделать это: -)

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

Ответ 1

Я нашел способ сделать это для сериализуемых лямбда. Все мои лямбды являются сериализуемыми, и это работает.

Спасибо, Хольгер, за то, что указал мне на SerializedLambda.

Общие параметры фиксируются в синтетическом статическом методе лямбда и могут быть получены оттуда. Поиск статического метода, реализующего лямбда, возможно с помощью информации из SerializedLambda

Шаги следующие:

  • Получить SerializedLambda с помощью метода замены записи, который автоматически сгенерирован для всех сериализуемых lambdas
  • Найти класс, содержащий реализацию лямбда (как синтетический статический метод)
  • Получить java.lang.reflect.Method для синтетического статического метода
  • Получить общие типы из этого Method

ОБНОВЛЕНИЕ: По-видимому, это не работает со всеми компиляторами. Я пробовал его с помощью компилятора Eclipse Luna (works) и Oracle javac (не работает).


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

Ответ 2

Точное решение о том, как сопоставить лямбда-код с реализацией интерфейса, остается в реальной среде выполнения. В принципе, все lambdas, реализующие один и тот же raw-интерфейс, могут совместно использовать один класс времени выполнения, как это делает MethodHandleProxies. Использование разных классов для конкретных lambdas - это оптимизация, выполняемая фактической реализацией LambdaMetafactory, но не функция, предназначенная для помощи в отладке или отражении.

Таким образом, даже если вы найдете более подробную информацию в реальном классе выполнения для реализации лямбда-интерфейса, это будет артефактом используемой среды выполнения, которая может быть недоступна в разных реализациях или даже в других версиях вашей текущей среды.

Если лямбда Serializable, вы можете использовать тот факт, что сериализованная форма содержит подпись типа экземпляра интерфейса, чтобы свести к минимуму значения переменных фактического типа.

Ответ 3

В настоящее время это можно решить, но только в довольно хаки, но позвольте мне сначала объяснить несколько вещей:

Когда вы пишете лямбда, компилятор вводит инструкцию динамического вызова, указывающую на LambdaMetafactory и частный статический синтетический метод с телом лямбда. Синтетический метод и дескриптор метода в постоянном пуле содержат общий тип (если лямбда использует этот тип или явственна, как в ваших примерах).

Теперь во время выполнения вызывается LambdaMetaFactory и генерируется класс с использованием ASM, который реализует функциональный интерфейс, а тело метода затем вызывает частный статический метод с любыми переданными аргументами. Затем он вводится в исходный класс, используя Unsafe.defineAnonymousClass (см. сообщение Джона Роуза), чтобы он мог получить доступ к закрытым членам и т.д.

К сожалению, сгенерированный класс не сохраняет общие подписи (он может), поэтому вы не можете использовать обычные методы отражения, которые позволяют обойти стирание

Для обычного класса вы можете проверить байт-код с помощью Class.getResource(ClassName + ".class"), но для анонимных классов, определенных с помощью Unsafe, вам не повезло. Однако вы можете сделать LambdaMetaFactory сброс их с помощью аргумента JVM:

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

Изучив файл сбрасываемого класса (используя javap -p -s -v), можно увидеть, что он действительно вызывает статический метод. Но проблема остается в том, как получить байт-код из самой Java.

К сожалению, это хакки:

Используя отражение, мы можем вызвать Class.getConstantPool, а затем получить доступ к MethodRefInfo, чтобы получить дескрипторы типа. Затем мы можем использовать ASM для синтаксического анализа этого и возвращения типов аргументов. Объединяя все это:

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

Обновлено с предложением jonathan

Теперь в идеале классы, сгенерированные LambdaMetaFactory, должны хранить сигнатуры общего типа (я мог бы увидеть, могу ли я предоставить патч OpenJDK), но в настоящее время это лучшее, что мы можем сделать. В приведенном выше коде есть следующие проблемы:

  • Он использует недокументированные методы и классы
  • Он чрезвычайно уязвим для изменений кода в JDK
  • Он не сохраняет общие типы, поэтому, если вы передадите List <String> в лямбда он выйдет как List

Ответ 4

Информация с параметризованным типом доступна только во время выполнения для элементов связанного кода, то есть специально скомпилированного в тип. Лямбдас делает то же самое, но поскольку ваша Лямбда не привязана к методу, а не к типу, нет никакого типа для захвата этой информации.

Рассмотрим следующее:

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 и f1 сохраняют общую информацию типа, как и следовало ожидать. Но поскольку они являются несвязанными методами, которые были стерты до Function<Object,Object>, f2 и f3, не делайте этого.

Ответ 5

Недавно я добавил поддержку для разрешения аргументов лямбда-типа для TypeTools. Пример:

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

Аргументы разрешенных типов как ожидалось:

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

Для обработки пройденной лямбда:

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

Примечание. В базовой реализации используется подход ConstantPool, описанный @danielbodart, который, как известно, работает с Oracle JDK и OpenJDK (и, возможно, с другими). ​​