Javaのメソッドハンドル

Javaのメソッドハンドル

1. 前書き

この記事では、Java 7で導入され、次のバージョンで拡張された重要なAPI、java.lang.invoke.MethodHandlesについて説明します。

特に、メソッドハンドルとは何か、それらを作成する方法、およびそれらを使用する方法を学習します。

2. メソッドハンドルとは何ですか?

APIドキュメントに記載されているように、その定義に来る:

メソッドハンドルは、引数または戻り値のオプションの変換を伴う、基になるメソッド、コンストラクター、フィールド、または同様の低レベル操作への型付きの直接実行可能な参照です。

より簡単な方法では、method handles are a low-level mechanism for finding, adapting and invoking methodsです。

メソッドハンドルは不変であり、表示状態はありません。

MethodHandleを作成して使用するには、次の4つの手順が必要です。

  • ルックアップを作成する

  • メソッドタイプの作成

  • メソッドハンドルの検索

  • メソッドハンドルの呼び出し

2.1. メソッドハンドルとリフレクション

メソッドハンドルは、既存のjava.lang.reflect APIと連携するために導入されました。これらは、さまざまな目的を果たし、さまざまな特性を備えているためです。

パフォーマンスの観点から、MethodHandles API can be much faster than the Reflection API since the access checks are made at creation time rather than at execution time。 メンバーとクラスのルックアップは追加のチェックの対象となるため、セキュリティマネージャーが存在する場合、この違いは増幅されます。

ただし、パフォーマンスだけがタスクの適合性の尺度ではないことを考慮すると、メンバークラスの列挙、アクセシビリティフラグの検査などのメカニズムがないため、MethodHandlesAPIの使用が難しいことも考慮する必要があります。 。

それでも、MethodHandles APIは、メソッドをカレーし、パラメーターのタイプを変更し、それらの順序を変更する可能性を提供します。

MethodHandles APIの明確な定義と目標があれば、ルックアップから始めて、それらを操作し始めることができます。

3. Lookupの作成

メソッドハンドルを作成するときに最初に行うことは、ルックアップを取得することです。ルックアップは、ルックアップクラスに表示されるメソッド、コンストラクタ、およびフィールドのメソッドハンドルを作成するファクトリオブジェクトです。

MethodHandles APIを使用すると、さまざまなアクセスモードでルックアップオブジェクトを作成できます。

publicメソッドへのアクセスを提供するルックアップを作成しましょう。

MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

ただし、privateメソッドとprotectedメソッドにもアクセスしたい場合は、代わりにlookup()メソッドを使用できます。

MethodHandles.Lookup lookup = MethodHandles.lookup();

4. MethodTypeの作成

MethodHandleを作成できるようにするには、ルックアップオブジェクトにそのタイプの定義が必要であり、これはMethodTypeクラスを介して実現されます。

特に、a MethodType represents the arguments and return type accepted and returned by a method handle or passed and expected by a method handle caller

MethodTypeの構造は単純であり、戻り値の型と、メソッドハンドルとそのすべての呼び出し元の間で適切に一致する必要がある適切な数のパラメーター型によって形成されます。

MethodHandleと同じように、MethodTypeのインスタンスでさえ不変です。

戻り値の型としてjava.util.Listクラスを指定し、入力型としてObject配列を指定するMethodTypeを定義する方法を見てみましょう。

MethodType mt = MethodType.methodType(List.class, Object[].class);

メソッドがプリミティブ型またはvoidを戻り値の型として返す場合、それらの型を表すクラス(void.class、int.class…)を使用します。

int値を返し、Objectを受け入れるMethodTypeを定義しましょう。

MethodType mt = MethodType.methodType(int.class, Object.class);

これで、MethodHandleの作成に進むことができます。

5. MethodHandleを見つける

メソッドタイプを定義したら、MethodHandle,を作成するために、lookupまたはpublicLookupオブジェクトを介してそれを見つけ、オリジンクラスとメソッド名も指定する必要があります。

特に、ルックアップファクトリはset of methodsを提供します。これにより、メソッドのスコープを考慮して適切な方法でメソッドハンドルを見つけることができます。 最も単純なシナリオから始めて、主要なシナリオを見ていきましょう。

5.1. メソッドのメソッドハンドル

findVirtual()メソッドを使用すると、オブジェクトメソッドのMethodHandleを作成できます。 Stringクラスのconcat()メソッドに基づいて作成しましょう。

MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);

5.2. 静的メソッドのメソッドハンドル

静的メソッドにアクセスしたい場合は、代わりにfindStatic()メソッドを使用できます。

MethodType mt = MethodType.methodType(List.class, Object[].class);

MethodHandle asListMH = publicLookup.findStatic(Arrays.class, "asList", mt);

この場合、Objectsの配列をそれらのListに変換するメソッドハンドルを作成しました。

5.3. コンストラクターのメソッドハンドル

コンストラクターへのアクセスの取得は、findConstructor()メソッドを使用して実行できます。

String属性を受け入れて、Integerクラスのコンストラクターとして動作するメソッドハンドルを作成しましょう。

MethodType mt = MethodType.methodType(void.class, String.class);

MethodHandle newIntegerMH = publicLookup.findConstructor(Integer.class, mt);

5.4. フィールドのメソッドハンドル

メソッドハンドルを使用すると、フィールドにもアクセスできます。

Bookクラスの定義を始めましょう:

public class Book {

    String id;
    String title;

    // constructor

}

メソッドハンドルと宣言されたプロパティ間の直接アクセスの可視性を前提条件として、ゲッターとして動作するメソッドハンドルを作成できます。

MethodHandle getTitleMH = lookup.findGetter(Book.class, "title", String.class);

変数/フィールドの処理の詳細については、Java 9 Variable Handles Demystifiedを参照してください。ここでは、Java 9で追加されたjava.lang.invoke.VarHandleAPIについて説明します。

5.5. プライベートメソッドのメソッドハンドル

プライベートメソッドのメソッドハンドルの作成は、java.lang.reflectAPIを使用して実行できます。

privateメソッドをBookクラスに追加してみましょう。

private String formatBook() {
    return id + " > " + title;
}

これで、formatBook()メソッドとまったく同じように動作するメソッドハンドルを作成できます。

Method formatBookMethod = Book.class.getDeclaredMethod("formatBook");
formatBookMethod.setAccessible(true);

MethodHandle formatBookMH = lookup.unreflect(formatBookMethod);

6. メソッドハンドルの呼び出し

メソッドハンドルを作成したら、それを使用することが次のステップです。 特に、MethodHandleクラスは、メソッドハンドルを実行するための3つの異なる方法を提供します:invoke(),invokeWithArugments()およびinvokeExact()

invokeオプションから始めましょう。

6.1. メソッドハンドルの呼び出し

invoke()メソッドを使用する場合、引数の数(アリティ)を固定するように強制しますが、引数と戻り値の型のキャストとボックス化/ボックス化解除の実行を許可します。

ボックス化された引数でinvoke()を使用する方法を見てみましょう。

MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);

String output = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');

assertEquals("java", output);

この場合、replaceMHにはchar引数が必要ですが、invoke()は、実行前にCharacter引数に対してボックス化解除を実行します。

6.2. 引数を使用した呼び出し

invokeWithArgumentsメソッドを使用してメソッドハンドルを呼び出すことは、3つのオプションの中で最も制限が少ないです。

実際、引数と戻り値型のキャストとボックス化/ボックス化解除に加えて、可変アリティ呼び出しが可能です。

練習に入ると、これにより、int値のarrayから開始してIntegerListを作成できます。

MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asList = publicLookup.findStatic(Arrays.class, "asList", mt);

List list = (List) asList.invokeWithArguments(1,2);

assertThat(Arrays.asList(1,2), is(list));

6.3. 正確な呼び出し

メソッドハンドル(引数の数とそのタイプ)を実行する方法をより制限したい場合は、invokeExact()メソッドを使用する必要があります。

実際、提供されたクラスへのキャストは提供されず、固定数の引数が必要です。

メソッドハンドルを使用して、2つのint値をsumする方法を見てみましょう。

MethodType mt = MethodType.methodType(int.class, int.class, int.class);
MethodHandle sumMH = lookup.findStatic(Integer.class, "sum", mt);

int sum = (int) sumMH.invokeExact(1, 11);

assertEquals(12, sum);

この場合、intではない数値をinvokeExactメソッドに渡すことにした場合、呼び出しはWrongMethodTypeException.になります。

7. 配列の操作

MethodHandlesは、フィールドやオブジェクトだけでなく、配列でも機能することを目的としています。 実際のところ、asSpreader() APIを使用すると、配列拡散メソッドを処理することができます。

この場合、メソッドハンドルは配列引数を受け入れ、その要素を位置引数として、オプションで配列の長さを広げます。

メソッドハンドルを広げて、配列内の要素が等しいかどうかを確認する方法を見てみましょう。

MethodType mt = MethodType.methodType(boolean.class, Object.class);
MethodHandle equals = publicLookup.findVirtual(String.class, "equals", mt);

MethodHandle methodHandle = equals.asSpreader(Object[].class, 2);

assertTrue((boolean) methodHandle.invoke(new Object[] { "java", "java" }));

8. メソッドハンドルの拡張

メソッドハンドルを定義したら、実際に呼び出さずにメソッドハンドルを引数にバインドすることで、メソッドハンドルを拡張できます。

たとえば、Java 9では、この種の動作はStringの連結を最適化するために使用されます。

接尾辞をconcatMHにバインドして、連結を実行する方法を見てみましょう。

MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);

MethodHandle bindedConcatMH = concatMH.bindTo("Hello ");

assertEquals("Hello World!", bindedConcatMH.invoke("World!"));

9. Java9の機能強化

Java 9では、使用をはるかに簡単にすることを目的として、MethodHandlesAPIにいくつかの機能拡張が行われました。

機能強化は、3つの主要なトピックに影響を与えました。

  • Lookup functions –さまざまなコンテキストからのクラスルックアップを可能にし、インターフェイスで非抽象メソッドをサポートします

  • Argument handling –引数の折りたたみ、引数の収集、および引数の拡散機能の改善

  • Additional combinations –ループの追加(loopwhileLoop,doWhileLoop…)およびtryFinallyによるより優れた例外処理サポート

これらの変更により、いくつかの追加の利点がもたらされました。

  • JVMコンパイラーの最適化の向上

  • インスタンス化の削減

  • MethodHandlesAPIの使用の精度を有効にしました

行われた拡張の詳細は、MethodHandles API Javadocで入手できます。

10. 結論

この記事では、MethodHandles APIとは何か、およびそれらの使用方法について説明しました。

また、Reflection APIとの関係についても説明しました。メソッドハンドルは低レベルの操作を許可するため、ジョブの範囲に完全に適合しない限り、それらを使用しない方が良いでしょう。

いつものように、この記事の完全なソースコードはover on Githubで入手できます。