我正在Java 8中尝试新的Lambdas,我正在寻找一种方法来使用lambda类的反射来获取lambda函数的返回类型。我特别感兴趣的是lambda实现了一个通用的超接口。在下面的代码示例中,MapFunction<F, T>
是通用的超接口,我正在寻找一种方法来找出绑定到泛型参数T
的类型。
虽然Java在编译器之后抛弃了许多泛型类型信息,但是泛型超类和通用超接口的子类(和匿名子类)确实保留了该类型信息。通过反射,可以访问这些类型。在下面的示例(案例1)中,反射告诉我MyMapper
的{{1}}实现将MapFunction
绑定到泛型类型参数java.lang.Integer
。
即使对于本身是通用的子类,也有一些方法可以找出与泛型参数绑定的内容,如果已知其他参数。考虑以下示例中的案例2 ,T
IdentityMapper
和F
绑定到同一类型。当我们知道这一点时,如果我们知道参数类型T
(我的情况就是这样),我们知道类型F
。
现在的问题是,如何才能实现Java 8 lambdas的类似功能?由于它们实际上不是通用超接口的常规子类,因此上述方法不起作用。
具体来说,我是否可以确定T
将parseLambda
绑定到java.lang.Integer
,而T
将identityLambda
绑定到F
和T
?< / p>
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;
}
答案 0 :(得分:17)
如何将lambda代码映射到接口实现的确切决策留给实际的运行时环境。原则上,实现相同原始接口的所有lambda都可以像MethodHandleProxies
那样共享一个运行时类。对特定lambdas使用不同的类是由实际的LambdaMetafactory
实现执行的优化,而不是用于帮助调试或反射的功能。
因此,即使您在lambda接口实现的实际运行时类中找到更详细的信息,它也将是当前使用的运行时环境的工件,它可能在不同的实现中甚至在当前环境的其他版本中都不可用。 / p>
如果lambda是Serializable
,您可以使用serialized form包含实例化接口类型的method signature这一事实来将实际类型变量值拼接在一起。
答案 1 :(得分:14)
这目前可以解决,但只是以一种漂亮的hackie方式,但我先解释一下:
编写lambda时,编译器会插入一个指向LambdaMetafactory的动态调用指令和一个带有lambda主体的私有静态合成方法。常量池中的合成方法和方法句柄都包含泛型类型(如果lambda使用类型或在示例中是显式的)。
现在在运行时调用LambdaMetaFactory
并使用实现功能接口的ASM生成类,然后该方法的主体使用传递的任何参数调用私有静态方法。然后使用Unsafe.defineAnonymousClass
将其注入原始类(请参阅John Rose post),以便它可以访问私有成员等。
不幸的是,生成的类没有存储通用签名(它可以),所以你不能使用通常的反射方法来解决擦除问题
对于普通类,您可以使用Class.getResource(ClassName + ".class")
检查字节码,但对于使用Unsafe
定义的匿名类,您运气不好。但是,您可以使用JVM参数将LambdaMetaFactory
转储出来:
java -Djdk.internal.lambda.dumpProxyClasses=/some/folder
通过查看转储的类文件(使用javap -p -s -v
),可以看到它确实调用了静态方法。但问题仍然是如何从Java本身获取字节码。
不幸的是,它变成了hackie:
使用反射我们可以调用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提交补丁)但是目前这是我们能做的最好的。上面的代码有以下问题:
答案 2 :(得分:11)
参数化类型信息仅在运行时可用于绑定的代码元素 - 即,特别编译为类型。 Lambdas做同样的事情,但是当你的Lambda被去掉一个方法而不是一个类型时,没有类型来捕获这些信息。
请考虑以下事项:
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
没有。
答案 3 :(得分:11)
我最近添加了对解析TypeTools的lambda类型参数的支持。例如:
MapFunction<String, Integer> fn = str -> Integer.valueOf(str);
Class<?>[] typeArgs = TypeResolver.resolveRawArguments(MapFunction.class, fn.getClass());
已解析的类型args符合预期:
assert typeArgs[0] == String.class;
assert typeArgs[1] == Integer.class;
处理传递的lambda:
public void call(Callable<?> c) {
// Assumes c is a lambda
Class<?> callableType = TypeResolver.resolveRawArguments(Callable.class, c.getClass());
}
注意:底层实现使用@danielbodart概述的ConstantPool方法,该方法已知可用于Oracle JDK和OpenJDK(可能还有其他方法)。
答案 4 :(得分:7)
我找到了一种为可序列化的lambdas做这种方法的方法。我的所有lambdas都是可序列化的,这就是可行的。
谢谢,Holger,指点我SerializedLambda
。
泛型参数在lambda的合成静态方法中捕获,可以从那里检索。使用SerializedLambda
步骤如下:
java.lang.reflect.Method
Method
更新:显然,这不适用于所有编译器。我已经使用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$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");
}