web-dev-qa-db-ja.com

Java 8ラムダでの反射型推論

Java 8で新しいLambdasを試していました。ラムダ関数の戻り値の型を取得するためにラムダクラスでリフレクションを使用する方法を探しています。特にケースに興味があります。以下のコード例では、MapFunction<F, T>が汎用スーパーインターフェイスであり、どのタイプが汎用パラメーターTにバインドされているかを調べる方法を探しています。

Javaはコンパイラー、ジェネリックスーパークラスおよびジェネリックスーパーインターフェイスのサブクラス(および匿名サブクラス)がそのタイプ情報を保持した後、多くのジェネリックタイプ情報を破棄します。リフレクションにより、これらのタイプにアクセスできました。以下の例(ケース1)、リフレクションは、MyMapperMapFunction実装が、Java.lang.Integerをジェネリック型パラメーターTにバインドすることを示しています。

サブクラス自体がジェネリックである場合でも、他のサブクラスがわかっている場合、ジェネリックパラメータにバインドするものを特定する特定の手段があります。以下の例でcase 2を検討してください。IdentityMapperFの両方が同じ型にバインドされるTです。それを知っているとき、パラメータ型Fを知っていれば、タイプTを知っています(私の場合はそうしています)。

ここでの質問は、Java 8ラムダに似たものをどのように実現できますか?これらは実際には汎用スーパーインターフェースの通常のサブクラスではないため、上記の方法は機能しません。 parseLambdaJava.lang.IntegerTにバインドし、identityLambdaFおよび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;
}
48
Stephan Ewen

シリアライズ可能なラムダに対してそれを行う方法を見つけました。私のすべてのラムダはシリアル化可能で、それが機能します。

Holger、SerializedLambdaを指し示してくれてありがとう。

ジェネリックパラメーターはラムダの合成静的メソッドでキャプチャされ、そこから取得できます。 SerializedLambdaからの情報を使用して、ラムダを実装する静的メソッドを見つけることができます。

手順は次のとおりです。

  1. すべてのシリアル化可能なラムダに対して自動生成される書き込み置換メソッドを介してSerializedLambdaを取得します
  2. (合成静的メソッドとして)ラムダ実装を含むクラスを見つけます
  3. Java.lang.reflect.Method合成静的メソッドの場合
  4. その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");
}
7
Stephan Ewen

ラムダコードをインターフェイス実装にマッピングする方法の正確な決定は、実際のランタイム環境に委ねられます。原則として、同じ生のインターフェースを実装するすべてのラムダは、 MethodHandleProxies のように単一のランタイムクラスを共有できます。特定のラムダに異なるクラスを使用することは、実際のLambdaMetafactory実装によって実行されるoptimizationですが、デバッグやリフレクションを支援するための機能ではありません。

したがって、ラムダインターフェイス実装の実際のランタイムクラスでより詳細な情報を見つけたとしても、現在使用されているランタイム環境のアーティファクトであり、異なる実装や現在の環境の他のバージョンでは使用できない場合があります。

ラムダがSerializableである場合、 serialized form にはインスタンス化されたインターフェイスタイプの method signature が含まれているという事実を使用して、実際のタイプ変数値を一緒にパズルできます。 。

17
Holger

これは現在解決することは可能ですが、かなりハッキーな方法でのみ可能ですが、最初にいくつかのことを説明します。

ラムダを記述すると、コンパイラは 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にパッチを提出できるかどうかはわかります)が、現在これが最善です。上記のコードには次の問題があります。

  • 文書化されていないメソッドとクラスを使用します
  • JDKのコード変更に対して非常に脆弱です。
  • ジェネリック型は保持されないため、List <String>をラムダに渡すと、Listとして出力されます

パラメータ化された型情報は、バインドされているコードの要素、つまり、特定の型にコンパイルされた要素に対してのみ実行時に利用できます。ラムダは同じことをしますが、ラムダは型ではなくメソッドにデシュガーされるため、その情報をキャプチャする型はありません。

以下を考慮してください。

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()));
        }
    }
}

f0f1はどちらも、期待どおり、ジェネリック型情報を保持します。しかし、それらはFunction<Object,Object>に消去された非バインドメソッドなので、f2およびf3は消去されません。

11
MrPotes

最近、ラムダ型の引数を解決するためのサポートを 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(および場合によってはその他)で動作することが知られています。

11
Jonathan