0

Вывод типа с помощью рефлексии для лямбд в Java 8

4

Заголовок: Как получить возвращаемый тип лямбда-функции с использованием рефлексии в Java 8?

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

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

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

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

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

/**
 * Суперинтерфейс.
 */
public interface MapFunction&lt;F, T&gt; {
    T map(F value);
}

/**
 * Случай 1: Несуществующий обобщённый подкласс.
 */
public class MyMapper implements MapFunction&lt;String, Integer&gt; {
    public Integer map(String value) {
        return Integer.valueOf(value);
    }
}

/**
 * Обобщённый подкласс
 */
public class IdentityMapper&lt;E&gt; implements MapFunction&lt;E, E&gt; {
    public E map(E value) {
        return value;
    }
}

/**
 * Инстанцирование через лямбду
 */
public MapFunction&lt;String, Integer&gt; parseLambda = (String str) -&gt; { return Integer.valueOf(str); }
public MapFunction&lt;E, E&gt; identityLambda = (value) -&gt; { return value; }

public static void main(String[] args) {
    // случай 1
    getReturnType(MyMapper.class);    // -&gt; возвращает java.lang.Integer

    // случай 2
    getReturnTypeRelativeToParameter(IdentityMapper.class, String.class);    // -&gt; возвращает java.lang.String
}

private static Class&lt;?&gt; getReturnType(Class&lt;?&gt; implementingClass) {
    Type superType = implementingClass.getGenericInterfaces()[0];

    if (superType instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) superType;
        return (Class&lt;?&gt;) parameterizedType.getActualTypeArguments()[1];
    } else return null;
}

private static Class&lt;?&gt; getReturnTypeRelativeToParameter(Class&lt;?&gt; implementingClass, Class&lt;?&gt; parameterType) {
    Type superType = implementingClass.getGenericInterfaces()[0];

    if (superType instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) superType;
        TypeVariable&lt;?&gt; inputType = (TypeVariable&lt;?&gt;) parameterizedType.getActualTypeArguments()[0];
        TypeVariable&lt;?&gt; returnType = (TypeVariable&lt;?&gt;) parameterizedType.getActualTypeArguments()[1];

        if (inputType.getName().equals(returnType.getName())) {
            return parameterType;
        } else {
            // некоторая логика, которая определяет составные типы возврата
        }
    }

    return null;
}

Буду рад любым советам или информации о том, как решить данную проблему!

2 ответ(ов)

0

Вопрос касается информации о параметризованных типах в Java и того, как лямбда-выражения обрабатываются компилятором. Давайте более подробно разберем ваш пример.

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

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

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, f2, f3). f0 и f1 сохраняют информацию о параметризованных типах, как и следовало ожидать, так как это явно определенные классы (в первом случае — через RetainedFunction, во втором — через анонимный класс).

Однако f2 и f3 работают как методы (в случае f2 используется ссылка на метод, а в случае f3 — лямбда-выражение), что подразумевает, что компилятор не связывает их с конкретным параметрическим типом. В результате, во время выполнения f2 и f3 у них появляется тип Function<Object, Object>, и именно поэтому информация о типах не сохраняется.

Когда программа пытается получить метод apply с аргументом типа Integer, это приводит к ошибке NoSuchMethodException для f2 и f3, так как они фактически считаются методами без конкретного типового определения. Это и есть иллюстрация стирания типов в Java, которая происходит на этапе компиляции для лямбда-выражений и ссылок на методы.

0

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

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

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

Вот шаги для достижения этой цели:

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

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


// пример использования
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);
    }

    // выводит следующее
    // (метод) private static java.util.List test.ClassWithLambdas.lambda$0(java.util.Set)
    // (возвращаемый тип, включая *Long* как тип обобщенного списка) java.util.List<java.lang.Long>
    // (параметр, включая *Double* как тип обобщенного множества) java.util.Set<java.lang.Double>
}

// получение 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) {
            // продолжаем цикл и пробуем следующий класс
        }
        catch (Throwable t) {
            throw new RuntimeException("Ошибка при извлечении сериализованной лямбды", t);
        }
    }

    throw new Exception("Метод writeReplace не найден");
}

// получение синтетического статического метода лямбды
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("Метод лямбды не найден");
}

Надеюсь, эта информация поможет вам!

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь