Java 8 Lambda 上的反射类型推断
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/21887358/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Reflection type inference on Java 8 Lambdas
提问by Stephan Ewen
I was experimenting with the new Lambdas in Java 8, and I am looking for a way to use reflection on the lambda classes to get the return type of a lambda function. I am especially interested in cases where the lambda implements a generic superinterface. In the code example below, MapFunction<F, T>
is the generic superinterface, and I am looking for a way to find out what type binds to the generic parameter T
.
我正在尝试使用 Java 8 中的新 Lambda,我正在寻找一种方法来在 lambda 类上使用反射来获取 lambda 函数的返回类型。我对 lambda 实现通用超接口的情况特别感兴趣。在下面的代码示例中,MapFunction<F, T>
是泛型超接口,我正在寻找一种方法来找出绑定到泛型参数的类型T
。
While Java throws away a lot of generic type information after the compiler, subclasses (and anonymous subclasses) of generic superclasses and generic superinterfaces did preserve that type information. Via reflection, these types were accessible. In the example below (case 1), reflection tells my that the MyMapper
implementation of MapFunction
binds java.lang.Integer
to the generic type parameter T
.
虽然 Java 在编译器之后丢弃了很多泛型类型信息,但泛型超类和泛型超接口的子类(和匿名子类)确实保留了这些类型信息。通过反射,这些类型是可以访问的。在下面的例子(情况1) ,反射告诉我,所述MyMapper
的实施MapFunction
结合java.lang.Integer
到一般类型参数T
。
Even for subclasses that are themselves generic, there are certain means to find out what binds to a generic parameter, if some others are known. Consider case 2in the example below, the IdentityMapper
where both F
and T
bind to the same type. When we know that, we know the type F
if we know the parameter type T
(which in my case we do).
即使对于本身是泛型的子类,如果知道其他一些参数,也可以通过某些方法找出绑定到泛型参数的内容。考虑下面示例中的情况 2,IdentityMapper
whereF
和 都T
绑定到同一类型。当我们知道这一点时,F
如果我们知道参数类型T
(在我的情况下我们就是这样做的),我们就知道了类型。
The question is now, how can I realize something similar for the Java 8 lambdas? Since they are actually not regular subclasses of the generic superinterface, the above described method does not work.
Specifically, can I figure out that the parseLambda
binds java.lang.Integer
to T
, and the identityLambda
binds the same to F
and T
?
现在的问题是,我如何才能为 Java 8 lambda 实现类似的功能?由于它们实际上不是通用超接口的常规子类,因此上述方法不起作用。具体来说,我可以弄清楚parseLambda
绑定java.lang.Integer
到T
和identityLambda
绑定到F
和T
吗?
PS: In theory it should possible to decompile the lambda code and then use an embedded compiler (like the JDT) and tap into its type inference. I hope that there is a simpler way to do this ;-)
PS:理论上应该可以反编译 lambda 代码,然后使用嵌入式编译器(如 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;
}
采纳答案by Stephan Ewen
I have found a way of doing it for serializable lambdas. All my lambdas are serializable, to that works.
我找到了一种对可序列化 lambda 进行操作的方法。我所有的 lambdas 都是可序列化的,这很有效。
Thanks, Holger, for pointing me to the SerializedLambda
.
谢谢,霍尔格,把我指向SerializedLambda
.
The generic parameters are captured in the lambda's synthetic static method and can be retrieved from there. Finding the static method that implements the lambda is possible with the information from the SerializedLambda
泛型参数在 lambda 的合成静态方法中捕获,可以从那里检索。找到实现 lambda 的静态方法是可能的SerializedLambda
The steps are as follows:
步骤如下:
- Get the SerializedLambda via the write replacement method that is auto-generated for all serializable lambdas
- Find the class that contains the lambda implementation (as a synthetic static method)
- Get the
java.lang.reflect.Method
for the synthetic static method - Get generic types from that
Method
- 通过为所有可序列化 lambda 自动生成的写入替换方法获取 SerializedLambda
- 查找包含 lambda 实现的类(作为合成静态方法)
- 获取
java.lang.reflect.Method
合成静态方法 - 从中获取泛型类型
Method
UPDATE:Apparently, this does not work with all compilers. I have tried it with the compiler of Eclipse Luna (works) and the Oracle javac (does not work).
更新:显然,这不适用于所有编译器。我已经用 Eclipse Luna(有效)和 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// 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");
}
(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 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");
}
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()));
}
}
}
回答by MrPotes
Parameterized type information is only available at runtime for elements of code that are bound - that is, specifically compiled into a type. Lambdas do the same thing, but as your Lambda is de-sugared to a method rather than to a type, there is no type to capture that information.
参数化类型信息仅在运行时可用于绑定的代码元素 - 即专门编译为类型。Lambdas 做同样的事情,但是由于您的 Lambda 被去糖化为方法而不是类型,因此没有类型可以捕获该信息。
Consider the following:
考虑以下:
java -Djdk.internal.lambda.dumpProxyClasses=/some/folder
f0
and f1
both retain their generic type information, as you'd expect. But as they're unbound methods that have been erased to Function<Object,Object>
, f2
and f3
do not.
f0
并且f1
都保留了它们的通用类型信息,正如您所期望的。但是,当他们是已被擦除非绑定方法Function<Object,Object>
,f2
并f3
没有。
回答by Holger
The exact decision how to map lambda code to interface implementations is left to the actual runtime environment. In principle, all lambdas implementing the same raw interface could share a single runtime class just like MethodHandleProxies
does. Using different classes for specific lambdas is an optimizationperformed by the actual LambdaMetafactory
implementation but not a feature intended to aid debugging or Reflection.
如何将 lambda 代码映射到接口实现的确切决定留给了实际的运行时环境。原则上,实现相同原始接口的所有 lambda 表达式都可以像MethodHandleProxies
这样共享一个运行时类。对特定的 lambda 使用不同的类是由实际实现执行的优化,LambdaMetafactory
而不是旨在帮助调试或反射的功能。
So even if you find more detailed information in the actual runtime class of a lambda interface implementation it will be an artifact of the currently used runtime environment which might not be available in different implementation or even other versions of your current environment.
因此,即使您在 lambda 接口实现的实际运行时类中找到更详细的信息,它也将是当前使用的运行时环境的工件,在不同的实现甚至您当前环境的其他版本中可能不可用。
If the lambda is Serializable
you can use the fact that the serialized formcontains the method signatureof the instantiated interface type to puzzle the actual type variable values together.
如果 lambda 是,Serializable
您可以使用序列化形式包含实例化接口类型的方法签名这一事实来将实际类型变量值拼凑在一起。
回答by Daniel Worthington-Bodart
This is currently possible to solve but only in a pretty hackie way, but let me first explain a few things:
这目前可以解决,但只能以一种非常hackie的方式解决,但让我首先解释一些事情:
When you write a lambda, the compiler inserts a dynamic invoke instruction pointing to the LambdaMetafactoryand a private static synthetic method with the body of the lambda. The synthetic method and the method handle in the constant pool both contain the generic type (if the lambda uses the type or is explicit as in your examples).
当您编写 lambda 时,编译器会插入一个指向LambdaMetafactory的动态调用指令和一个带有 lambda 主体的私有静态合成方法。常量池中的合成方法和方法句柄都包含泛型类型(如果 lambda 使用该类型或在您的示例中是显式的)。
Now at runtime the LambdaMetaFactory
is called and a class is generated using ASM that implements the functional interface and the body of the method then calls the private static method with any arguments passed. It is then injected into the original class using Unsafe.defineAnonymousClass
(see John Rose post) so it can access the private members etc.
现在在运行时LambdaMetaFactory
调用并使用 ASM 生成一个类,该类实现了功能接口和方法体,然后使用传递的任何参数调用私有静态方法。然后使用Unsafe.defineAnonymousClass
(参见John Rose 帖子)将其注入到原始类中,以便它可以访问私有成员等。
Unfortunately the generated Class does not store the generic signatures (it could) so you can't use the usual reflection methods that allow you to get around erasure
不幸的是,生成的类不存储通用签名(它可以)所以你不能使用通常的反射方法来绕过擦除
For a normal Class you could inspect the bytecode using Class.getResource(ClassName + ".class")
but for anonymous classes defined using Unsafe
you are out of luck. However you can make the LambdaMetaFactory
dump them out with the JVM argument:
对于普通类,您可以使用检查字节码,Class.getResource(ClassName + ".class")
但对于使用定义的匿名类,Unsafe
您就不走运了。但是,您可以LambdaMetaFactory
使用 JVM 参数将它们转储出来:
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);
By looking at the dumped class file (using javap -p -s -v
), one can see that it does indeed call the static method. But the problem remains how to get the bytecode from within Java itself.
通过查看转储的类文件(使用javap -p -s -v
),可以看到它确实调用了静态方法。但问题仍然是如何从 Java 内部获取字节码。
This unfortunately is where it gets hackie:
不幸的是,这就是它得到黑客的地方:
Using reflection we can call Class.getConstantPool
and then access the MethodRefInfo to get the type descriptors. We can then use ASM to parse this and return the argument types. Putting it all together:
使用反射,我们可以调用Class.getConstantPool
然后访问 MethodRefInfo 以获取类型描述符。然后我们可以使用 ASM 来解析它并返回参数类型。把它们放在一起:
MapFunction<String, Integer> fn = str -> Integer.valueOf(str);
Class<?>[] typeArgs = TypeResolver.resolveRawArguments(MapFunction.class, fn.getClass());
Updated with jonathan's suggestion
更新了乔纳森的建议
Now ideally the classes generated by LambdaMetaFactory
should store the generic type signatures (I might see if I can submit a patch to the OpenJDK) but currently this is the best we can do. The code above has the following problems:
现在理想情况下,由 生成的类LambdaMetaFactory
应该存储泛型类型签名(我可能会看到我是否可以向 OpenJDK 提交补丁),但目前这是我们能做的最好的事情。上面的代码存在以下问题:
- It uses undocumented methods and classes
- It is extremely vulnerable to code changes in the JDK
- It doesn't preserve the generic types, so if you pass List<String> into a lambda it will come out as List
- 它使用未记录的方法和类
- 它极易受到 JDK 中代码更改的影响
- 它不保留泛型类型,因此如果您将 List<String> 传递到 lambda 中,它将作为 List 出现
回答by Jonathan
I recently added support for resolving lambda type arguments to TypeTools. Ex:
我最近添加了对解析 lambda 类型参数的支持TypeTools。前任:
assert typeArgs[0] == String.class;
assert typeArgs[1] == Integer.class;
The resolved type args are as expected:
解析的类型参数符合预期:
public void call(Callable<?> c) {
// Assumes c is a lambda
Class<?> callableType = TypeResolver.resolveRawArguments(Callable.class, c.getClass());
}
To handle a passed lambda:
要处理传递的 lambda:
##代码##Note: The underlying implementation uses the ConstantPool approach outlined by @danielbodart which is known to work on Oracle JDK and OpenJDK (and possibly others).
注意:底层实现使用 @danielbodart 概述的 ConstantPool 方法,该方法已知适用于 Oracle JDK 和 OpenJDK(可能还有其他)。