Вывод типа с помощью рефлексии для лямбд в Java 8
Заголовок: Как получить возвращаемый тип лямбда-функции с использованием рефлексии в Java 8?
Я экспериментировал с новыми лямбда-выражениями в Java 8 и пытаюсь выяснить, как с помощью рефлексии узнать возвращаемый тип класса лямбда-функции. Особенно меня интересуют случаи, когда лямбда реализует обобщённый суперинтерфейс. В приведённом ниже примере <code>MapFunction<F, T></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<F, T> {
T map(F value);
}
/**
* Случай 1: Несуществующий обобщённый подкласс.
*/
public class MyMapper implements MapFunction<String, Integer> {
public Integer map(String value) {
return Integer.valueOf(value);
}
}
/**
* Обобщённый подкласс
*/
public class IdentityMapper<E> implements MapFunction<E, E> {
public E map(E value) {
return value;
}
}
/**
* Инстанцирование через лямбду
*/
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) {
// случай 1
getReturnType(MyMapper.class); // -> возвращает java.lang.Integer
// случай 2
getReturnTypeRelativeToParameter(IdentityMapper.class, String.class); // -> возвращает 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 {
// некоторая логика, которая определяет составные типы возврата
}
}
return null;
}
Буду рад любым советам или информации о том, как решить данную проблему!
2 ответ(ов)
Вопрос касается информации о параметризованных типах в 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, которая происходит на этапе компиляции для лямбда-выражений и ссылок на методы.
Я нашел способ сделать это для сериализуемых лямбд. Все мои лямбды сериализуемые, поэтому это работает.
Спасибо, Холгер, что указал мне на SerializedLambda
.
Обобщенные параметры захватываются в синтетическом статическом методе лямбды и могут быть получены оттуда. Найти статический метод, который реализует лямбду, можно, используя информацию из SerializedLambda
.
Вот шаги для достижения этой цели:
- Получите
SerializedLambda
с помощью метода замещения записи, который автоматически генерируется для всех сериализуемых лямбд. - Найдите класс, который содержит реализацию лямбды (как синтетический статический метод).
- Получите
java.lang.reflect.Method
для синтетического статического метода. - Получите обобщенные типы из этого
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("Метод лямбды не найден");
}
Надеюсь, эта информация поможет вам!
Как создать обобщённый массив в Java?
Java 8: Преобразование List<V> в Map<K, V>
Получить обобщённый тип класса во время выполнения
Java 8: Как использовать лямбда-функцию, которая выбрасывает исключение?
Java Лямбда-выражения [закрыто]