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

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

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

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

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

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

 /** * The superinterface. */ public interface MapFunction { T map(F value); } /** * Case 1: A non-generic subclass. */ public class MyMapper implements MapFunction { public Integer map(String value) { return Integer.valueOf(value); } } /** * A generic subclass */ public class IdentityMapper implements MapFunction { public E map(E value) { return value; } } /** * Instantiation through lambda */ public MapFunction parseLambda = (String str) -> { return Integer.valueOf(str); } public MapFunction 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; } 

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

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

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

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

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

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

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

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

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

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

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

Используя reflection, мы можем вызвать 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

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

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

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

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

 import java.util.Arrays; import java.util.function.Function; public class Erasure { static class RetainedFunction implements Function { public String apply(Integer t) { return String.valueOf(t); } } public static void main(String[] args) throws Exception { Function f0 = new RetainedFunction(); Function f1 = new Function() { public String apply(Integer t) { return String.valueOf(t); } }; Function f2 = String::valueOf; Function f3 = i -> String.valueOf(i); for (Function 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 , f2 и f3 , нет.

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

 MapFunction 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 (и, возможно, с другими).

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

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

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

Эти шаги заключаются в следующем:

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

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


 // sample how to use public static interface SomeFunction extends java.io.Serializable { List applyTheFunction(Set value); } public static void main(String[] args) throws Exception { SomeFunction 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 // (the parameter, including *Double* as the generic set type) java.util.Set 

 // 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"); } 
  • Как скопировать значение из classа X в class Y с тем же именем свойства в c #?
  • Почему рекомендуется использовать reflection в .NET?
  • В чем разница между a.getClass () и A.class в Java?
  • Найдите личное поле с Reflection?
  • Как определить, реализует ли тип определенного типа общего типа
  • Почему C ++ не имеет отражения?
  • Получить строковое имя свойства, используя reflection
  • Как перечислить все переменные classа
  • Как получить имена параметров метода в Java 8 с использованием отражения?
  • Получить экземпляр объекта-компаньона с новым API-интерфейсом Scala reflection
  • Как я могу динамически добавлять поле в class в C #
  • Давайте будем гением компьютера.