MethodHandlesで遊ぶ

初めに

今回出てくるのはこの辺

MethodHandleの朝はLookupオブジェクトを作ることから始まる。

Lookup lookup = MethodHandles.lookup();

フィールドを取得してみる。

  @Test
  public void test6() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findGetter(
      /* receiver type */ PublicTestObject.class,
      /* method name */ "stringField",
      /* return type */ String.class
    );
    assertThat(handle.invoke(new PublicTestObject("test"))).isEqualTo("test");
  }

  @AllArgsConstructor
  public static class PublicTestObject {
    public String stringField;
  }

はい。Lombokのアノテーションを使ってますが 大したコードじゃありません。 フィールドを参照するGetterとして、MethodHandle(関数みたいなもの)を取得してinvokeしています。

メソッドを実行してみる。

  @Test
  public void test1() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.bind(
      /* method receiver */ new TestObject(),
      /* method name */ "get",
      /* method signature */ MethodType.methodType(/* return type */ String.class)
    );
    assertThat(handle.invoke()).isEqualTo("getting.");
  }

  // 上記とほぼ等価
  // 中の処理は少し違うが、いまいち違いが分かっていない。
  // 中でinvokeSpecialみたいな文言が飛んでいるが、findSpecialと少し動きが違う・・・?
  @Test
  public void test2() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findVirtual(
      TestObject.class,
      "get",
      MethodType.methodType(String.class)
    );
    assertThat(handle.invoke(new TestObject())).isEqualTo("getting.");
  }


  @Test
  public void test3() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findVirtual(
      HasMethodObject.class,
      "getField",
      MethodType.methodType(String.class, String.class)
    );
    // 引数の部分適用
    MethodHandle handle2 = MethodHandles.insertArguments(handle, 1, "xxxx");
    assertThat(handle2.invoke(new HasMethodObject("yyy"))).isEqualTo("yyyxxxx");

  }
  @Test
  public void test4(){
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findVirtual(
      HasMethodObject.class,
      "getField",
      MethodType.methodType(String.class, String.class)
    );
    // 引数の再配置
    MethodHandle permuted = MethodHandles.permuteArguments(
      /* 再配置するMethodHandle */ handle,
      /* 再配置後のシグネチャ */ MethodType.methodType(String.class, String.class, HasMethodObject.class),
      /* 再配置前後の順序 */ 1, 0);
    assertThat(permuted.invoke("zzzz", new HasMethodObject("yyy"))).isEqualTo("yyyzzzz");
  }

  @Test
  public void test5() throws Throwable {
    Lookup lookup = MethodHandles.lookup();

    MethodType methodType = MethodType.genericMethodType(1, true)
      .changeReturnType(String.class)
      .changeParameterType(0, String.class);
    assertThat(methodType.toMethodDescriptorString())
      .isEqualTo("(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;");
    
    MethodHandle handle = lookup.findStatic(MessageFormat.class, "format", methodType);
    // 第一引数に部分適用
    MethodHandle formatter = handle.bindTo("format strings {0} {1}");

    // 可変長引数をいい感じに呼べるようにする
    assertThat(formatter.asVarargsCollector(Object[].class)
      .invoke("1", "2")).isEqualTo("format strings 1 2");

    // asVarargsCollectorを使わなかった場合:ダサい。
    assertThat(formatter.invoke(new Object[] {
      "1",
      "2"
    })).isEqualTo("format strings 1 2");
  }


  @Test
  public void test6() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodType methodType = MethodType.genericMethodType(1, true)
      .changeReturnType(String.class)
      .changeParameterType(0, String.class);

    // 呼び出す関数
    MethodHandle formatter = lookup.findStatic(MessageFormat.class, "format", methodType)
      .bindTo("format strings {0} {1}");
    // 前処理関数
    MethodHandle trace = lookup.findVirtual(PrintStream.class, "printf", MethodType.methodType(PrintStream.class, new Class[] {
      String.class,
      Object[].class
    }))
      .bindTo(System.out)
      .asType(MethodType.methodType(void.class, String.class, Object[].class))
      .bindTo("arguments %s %s");

    // also prints "arguments xxx yyy"
    assertThat(MethodHandles.foldArguments(formatter, trace)
      .invoke(new Object[] {
        "xxx",
        "yyy"
    })).isEqualTo("format strings xxx yyy");
  }

  @AllArgsConstructor
  public static class TestObject {

    public String get() {
      return "getting.";
    }
  }

  @AllArgsConstructor
  public static class HasMethodObject {
    private final String string;

    public String getField() {
      return string;
    }

    public String getField(String xxxx) {
      return string + xxxx;
    }
  }
  • メソッドの呼び出し
  • メソッド引数の部分適用
  • 関数引数の再配置
  • 前処理関数による関数の合成?いまいち言い方が分かりません。

色々触りました。 楽しそうなのは部分適用と引数の再配置ぐらいでしょうか?

SAM Type Proxy

  @SuppressWarnings("unused")
  private static boolean test(Object x) {
    return x instanceof String;
  }

  @Test
  public void test1() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findStatic(ProxiesTest.class, "test", MethodType.methodType(boolean.class, Object.class));

    @SuppressWarnings("unchecked")
    Predicate<Object> predicate = MethodHandleProxies.asInterfaceInstance(Predicate.class, handle);
    assertThat(predicate.test("test")).isEqualTo(true);
    assertThat(predicate.test(1)).isEqualTo(false);

    // デフォルトメソッドは呼び出せないみたい。
    assertThatThrownBy(predicate::negate).isInstanceOf(InternalError.class);
  }

単一メソッドを持つインターフェースのプロキシが簡単に作れる模様。 デフォルトメソッドは呼び出せない。 マジか。内部で元々ある、Proxy.newInstanceが使われてるみたいです。

終わり

簡単なコードを少し試してみましたが いまいち盛り上がりに欠ける感じになりました。 元々あるリフレクションと比べるとAPIJVMよりになっているように感じます。

また、ラムダと関連を伺えるようなオブジェクトがいくつか存在しています。

今回の記事はラムダ式がどうやって実行されるか、という話を調べた際の副産物です。

可変長引数の周りでだいぶ苦戦しました。

Enjoy java.

今回この記事で書いたコードはここに置いてます。 github.com

参考に見たリンク

java.lang.invoke で遊ぼう - Java読書会合宿2012 Java7でカリー化?(部分適用でした、、、) - tomoTakaの日記