かまたま日記3

プログラミングメイン、たまに日常

Lambdaオブジェクトの型パラメータを取るのは難しい

TL;DR

  • ラムダオブジェクトの型パラメータを取得するスマートな方法は今の所見つかっていない
  • もし基盤プログラムでそういうことをしたい場合は、ラムダを禁止して、匿名クラスを使う
  • いい方法があったら教えてください

本文

Javaで基盤プログラム的なのを作るとき、ジェネリクスの型パラメータを取得したいことがあります。普通のクラスや匿名クラスの場合は以下のようなリフレクションのコードで取得することができます。

import java.lang.reflect.ParameterizedType;
import java.util.function.Consumer;

public class FooTest {
    public static void main(String[] args) {
        System.out.println(getGenericTypeParam(new Foo()));
        System.out.println(getGenericTypeParam(new Bar()));
        System.out.println(getGenericTypeParam(Baz));
    }

    private static Class<?> getGenericTypeParam(Consumer consumer) {
        ParameterizedType type = (ParameterizedType) consumer.getClass().getGenericInterfaces()[0];
        return (Class) type.getActualTypeArguments()[0];
    }

    private static class Foo implements Consumer<String> {
        @Override
        public void accept(String s) {}
    }

    private static class Bar implements Consumer<Integer> {
        @Override
        public void accept(Integer s) {}
    }

    private static Consumer<Void> Baz = new Consumer<Void>() {
        @Override
        public void accept(Void aVoid) {
        }
    };
}

実行結果

class java.lang.String
class java.lang.Integer
class java.lang.Void

しかし、これがラムダになると、getGenericInterfaces メソッドの結果が ParameterizedType ではなく単なる java.lang.Object のクラス型になり、ClassCastExceptionが発生してしまいます。

getGenericTypeParam((Consumer<Byte>) (b -> {}));

結果

Exception in thread "main" java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType

つまり、ラムダ式で生成された関数オブジェクトからは型パラメータの情報が消えているのです。これをどうにかして取得したいといろいろ模索していたのですが、結局ダメでした。一番惜しかったのはこちらのGistのやり方です。

ラムダ式が定義されているクラスで getDeclaredMethods を使ってメソッド一覧を見ると、そのクラス内で定義されたラムダ式に対応したSyntheticメソッドが生成されています。

import java.lang.reflect.Method;

public class FooTest {
    public static void main(String[] args) {
        Runnable task1 = () -> task2();
        System.out.println(task1.getClass().getName());
        task1.run();

        System.out.println();
        for (Method method : FooTest.class.getDeclaredMethods()) {
            System.out.println(method);
        }
    }

    private static void task2() {
        Runnable task2 = () -> {};
        System.out.println(task2.getClass().getName());
    }
}

上記のプログラムを実行すると以下のような結果が出力されます。

FooTest$$Lambda$1/664223387
FooTest$$Lambda$2/666641942

public static void FooTest.main(java.lang.String[])
private static void FooTest.task2()
private static void FooTest.lambda$task2$1()
private static void FooTest.lambda$main$0()

ラムダ式で生成されたオブジェクトとSyntheticメソッドにはどちらも名前に $1 的なインデックスがついており、それぞれのインデックスが1対1で対応していそうです。そこで、先のGistに習って以下の getGenericTypeParamSmart を追加します。

private static Class<?> getGenericTypeParamSmart(Consumer consumer) {
    String functionClassName = consumer.getClass().getName();
    int lambdaMarkerIndex = functionClassName.indexOf("$$Lambda$");
    if (lambdaMarkerIndex == -1) { // Not a lambda
        return getGenericTypeParam(consumer);
    }

    String declaringClassName = functionClassName.substring(0, lambdaMarkerIndex);
    int lambdaIndex = Integer.parseInt(functionClassName.substring(lambdaMarkerIndex + 9, functionClassName.lastIndexOf('/')));

    Class<?> declaringClass;
    try {
        declaringClass = Class.forName(declaringClassName);
    } catch (ClassNotFoundException e) {
        throw new IllegalStateException("Unable to find lambda's parent class " + declaringClassName);
    }

    for (Method method : declaringClass.getDeclaredMethods()) {
        if (method.isSynthetic()
                && method.getName().startsWith("lambda$")
                && method.getName().endsWith("$" + (lambdaIndex - 1))
                && Modifier.isStatic(method.getModifiers())) {
            return method.getParameterTypes()[0];
        }
    }
    throw new IllegalStateException("Unable to find lambda's implementation method");
}

その上で、以下のコードを実行するとちゃんと型パラメータが取れてそうです

public static void main(String[] args) {
    System.out.println(getGenericTypeParamSmart(new Foo()));
    System.out.println(getGenericTypeParamSmart(new Bar()));
    System.out.println(getGenericTypeParamSmart(Baz));
    System.out.println(getGenericTypeParamSmart((Consumer<Byte>) (b -> {})));
    System.out.println(getGenericTypeParamSmart((Consumer<Long>) (l -> {})));
}
class java.lang.String
class java.lang.Integer
class java.lang.Void
class java.lang.Byte
class java.lang.Long

ただし、Gistにもコメントしましたが、ラムダ内でラムダを生成した場合、例えば、以下のパターンでは失敗します。

public static void main(String[] args) {
    Runnable task = () -> {
        System.out.println(getGenericTypeParamSmart(new Foo()));
        System.out.println(getGenericTypeParamSmart(new Bar()));
        System.out.println(getGenericTypeParamSmart(Baz));
        System.out.println(getGenericTypeParamSmart((Consumer<Byte>) (b -> {})));
        System.out.println(getGenericTypeParamSmart((Consumer<Long>) (b -> {})));
    };
    task.run();
}
class java.lang.String
class java.lang.Integer
class java.lang.Void
class java.lang.Long
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0

なぜでしょうか? ラムダオブジェクトとFooTestクラスに定義されているSyntheticメソッドを比較してみましょう

public static void main(String[] args) {
    Runnable task = () -> {
        Consumer<Byte> byteConsumer = b -> {};
        Consumer<Long> longConsumer = l -> {};
        System.out.println("byteConsumer: " + byteConsumer.getClass().getName());
        System.out.println("longConsumer: " + longConsumer.getClass().getName());
    };
    System.out.println("task: " + task.getClass().getName());
    task.run();

    System.out.println();
    for (Method method : FooTest.class.getDeclaredMethods()) {
        if (method.isSynthetic()) {
            System.out.println(method.toString());
        }
    }
}

これの実行結果は以下のようになります

task: FooTest$$Lambda$1/664223387
byteConsumer: FooTest$$Lambda$2/1349393271
longConsumer: FooTest$$Lambda$3/159413332

private static void FooTest.lambda$main$2()
private static void FooTest.lambda$null$1(java.lang.Long)
private static void FooTest.lambda$null$0(java.lang.Byte)

つまり:

  • task オブジェクト FooTest$$Lambda$1 に対応するSyntheticメソッドは lambda$main$2
  • byteConsumer オブジェクト FooTest$$Lambda$2 に対応するSyntheticメソッドは lambda$null$0
  • longConsumer オブジェクト FooTest$$Lambda$3 に対応するSyntheticメソッドは lambda$null$1

に、なるわけです。番号の対応がずれてるので、間違ったメソッドを検索してしまっていたわけです。そういう訳で、この方法は使えませんでした。

そのあといろいろ調べてみましたが、型パラメータをちゃんと取得する方法は見つかりませんでした。というわけで、こういうプログラムを書きたいときは今の所はラムダを禁止したほうが良さそうです。

private static Class<?> getGenericTypeParam(Consumer consumer) {
    String functionClassName = consumer.getClass().getName();
    if (functionClassName.contains("$$Lambda$")) {
        throw new UnsupportedOperationException("Lambda is not supported");
    }
    ParameterizedType type = (ParameterizedType) consumer.getClass().getGenericInterfaces()[0];
    return (Class) type.getActualTypeArguments()[0];
}

最終的なコードは以下のような感じになります。

import java.lang.reflect.ParameterizedType;
import java.util.function.Consumer;

public class FooTest {
    public static void main(String[] args) {
        System.out.println(getGenericTypeParam(new Foo()));
        System.out.println(getGenericTypeParam(new Bar()));
        System.out.println(getGenericTypeParam(Baz));
        try {
            System.out.println(getGenericTypeParam((Consumer<Byte>) (b -> {})));
        } catch (UnsupportedOperationException e) {}
    }

    private static Class<?> getGenericTypeParam(Consumer consumer) {
        String functionClassName = consumer.getClass().getName();
    if (functionClassName.contains("$$Lambda$")) {
        throw new UnsupportedOperationException("Lambda is not supported");
    }
        ParameterizedType type = (ParameterizedType) consumer.getClass().getGenericInterfaces()[0];
        return (Class) type.getActualTypeArguments()[0];
    }

    private static class Foo implements Consumer<String> {
        @Override
        public void accept(String s) {}
    }

    private static class Bar implements Consumer<Integer> {
        @Override
        public void accept(Integer s) {}
    }

    private static Consumer<Void> Baz = new Consumer<Void>() {
        @Override
        public void accept(Void aVoid) {}
    };
}