Java 8で新しいLambdasを試していました。ラムダ関数の戻り値の型を取得するためにラムダクラスでリフレクションを使用する方法を探しています。特にケースに興味があります。以下のコード例では、MapFunction<F, T>
が汎用スーパーインターフェイスであり、どのタイプが汎用パラメーターT
にバインドされているかを調べる方法を探しています。
Javaはコンパイラー、ジェネリックスーパークラスおよびジェネリックスーパーインターフェイスのサブクラス(および匿名サブクラス)がそのタイプ情報を保持した後、多くのジェネリックタイプ情報を破棄します。リフレクションにより、これらのタイプにアクセスできました。以下の例(ケース1)、リフレクションは、MyMapper
のMapFunction
実装が、Java.lang.Integer
をジェネリック型パラメーターT
にバインドすることを示しています。
サブクラス自体がジェネリックである場合でも、他のサブクラスがわかっている場合、ジェネリックパラメータにバインドするものを特定する特定の手段があります。以下の例でcase 2を検討してください。IdentityMapper
とF
の両方が同じ型にバインドされるT
です。それを知っているとき、パラメータ型F
を知っていれば、タイプT
を知っています(私の場合はそうしています)。
ここでの質問は、Java 8ラムダに似たものをどのように実現できますか?これらは実際には汎用スーパーインターフェースの通常のサブクラスではないため、上記の方法は機能しません。 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;
}
シリアライズ可能なラムダに対してそれを行う方法を見つけました。私のすべてのラムダはシリアル化可能で、それが機能します。
Holger、SerializedLambda
を指し示してくれてありがとう。
ジェネリックパラメーターはラムダの合成静的メソッドでキャプチャされ、そこから取得できます。 SerializedLambda
からの情報を使用して、ラムダを実装する静的メソッドを見つけることができます。
手順は次のとおりです。
Java.lang.reflect.Method
合成静的メソッドの場合Method
からジェネリック型を取得しますPDATE:どうやら、これはすべてのコンパイラで動作するとは限りません。 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");
}
ラムダコードをインターフェイス実装にマッピングする方法の正確な決定は、実際のランタイム環境に委ねられます。原則として、同じ生のインターフェースを実装するすべてのラムダは、 MethodHandleProxies
のように単一のランタイムクラスを共有できます。特定のラムダに異なるクラスを使用することは、実際のLambdaMetafactory
実装によって実行されるoptimizationですが、デバッグやリフレクションを支援するための機能ではありません。
したがって、ラムダインターフェイス実装の実際のランタイムクラスでより詳細な情報を見つけたとしても、現在使用されているランタイム環境のアーティファクトであり、異なる実装や現在の環境の他のバージョンでは使用できない場合があります。
ラムダがSerializable
である場合、 serialized form にはインスタンス化されたインターフェイスタイプの method signature が含まれているという事実を使用して、実際のタイプ変数値を一緒にパズルできます。 。
これは現在解決することは可能ですが、かなりハッキーな方法でのみ可能ですが、最初にいくつかのことを説明します。
ラムダを記述すると、コンパイラは LambdaMetafactory を指す動的呼び出し命令と、ラムダの本体を持つプライベート静的合成メソッドを挿入します。合成メソッドと定数プールのメソッドハンドルの両方にジェネリック型が含まれています(ラムダが型を使用する場合、または例のように明示的である場合)。
これで、実行時にLambdaMetaFactory
が呼び出され、機能インターフェイスとメソッドの本体を実装するASMを使用してクラスが生成され、引数が渡されたプライベートスタティックメソッドが呼び出されます。その後、_Unsafe.defineAnonymousClass
_を使用して元のクラスに注入されます( John Rose post を参照)。これにより、プライベートメンバーなどにアクセスできます。
残念ながら、生成されたクラスは一般的な署名を保存しないため(可能性があります)、通常のリフレクションメソッドを使用して消去を回避することはできません
通常のクラスの場合、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);
_
ジョナサンの提案で更新
理想的には、LambdaMetaFactory
によって生成されたクラスはジェネリック型署名を保存するはずです(OpenJDKにパッチを提出できるかどうかはわかります)が、現在これが最善です。上記のコードには次の問題があります。
パラメータ化された型情報は、バインドされているコードの要素、つまり、特定の型にコンパイルされた要素に対してのみ実行時に利用できます。ラムダは同じことをしますが、ラムダは型ではなくメソッドにデシュガーされるため、その情報をキャプチャする型はありません。
以下を考慮してください。
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
は消去されません。
最近、ラムダ型の引数を解決するためのサポートを 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());
}
注:基礎となる実装では、@ danielbodartで概説されているConstantJoolアプローチを使用します。これは、Oracle JDKおよびOpenJDK(および場合によってはその他)で動作することが知られています。