Вывод типа отражения на 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;
}
Ответы
Ответ 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 (и, возможно, с другими).