翻訳者から:LambdaMetafactoryは、おそらく最も過小評価されているJava 8メカニズムの1つであり、最近発見しましたが、すでにその機能を高く評価しています。 CUBAフレームワークのバージョン7.0では、ラムダ式の生成を優先してリフレクションコールを回避することにより、パフォーマンスが向上しています。 フレームワークにおけるこのメカニズムのアプリケーションの1つは、SpringのEventListenerの類似物であるアノテーション、共通タスクによるアプリケーションイベントハンドラーのバインドです。 LambdaFactoryの原則に関する知識は多くのJavaアプリケーションで役立つと信じており、この翻訳を急いで共有します。
この記事では、Java 8でラムダ式を使用する際のあまり知られていないトリックと、これらの式の制限を示します。 この記事の対象読者は、シニアJava開発者、研究者、ツールキット開発者です。 パブリックJava APIのみがcom.sun.*
なしで使用されcom.sun.*
また、他の内部クラスのため、コードは異なるJVM実装間で移植可能です。
短い序文
Java 8では、匿名メソッドを実装する方法としてラムダ式が登場しました。
場合によっては、匿名クラスの代替として。 バイトコードレベルでは、ラムダ式はinvokedynamic
置き換えられます。 この命令は、機能的インターフェースの実装を作成するために使用され、その唯一のメソッドは、ラムダ式の本体で定義されたコードを含む実際のメソッドへの呼び出しを委任します。
たとえば、次のコードがあります。
void printElements(List<String> strings){ strings.forEach(item -> System.out.println("Item = %s", item)); }
このコードは、Javaコンパイラによって次のようなものに変換されます。
private static void lambda_forEach(String item) { // Java System.out.println("Item = %s", item); } private static CallSite bootstrapLambda(Lookup lookup, String name, MethodType type) { // //lookup = VM //name = "lambda_forEach", VM //type = String -> void MethodHandle lambdaImplementation = lookup.findStatic(lookup.lookupClass(), name, type); return LambdaMetafactory.metafactory(lookup, "accept", MethodType.methodType(Consumer.class), // - MethodType.methodType(void.class, Object.class), // Consumer.accept lambdaImplementation, // - type); } void printElements(List<String> strings) { Consumer<String> lambda = invokedynamic# bootstrapLambda, #lambda_forEach strings.forEach(lambda); }
invokedynamic
命令は、そのようなJavaコードとして大まかに表すことができます。
private static CallSite cs; void printElements(List<String> strings) { Consumer<String> lambda; //begin invokedynamic if (cs == null) cs = bootstrapLambda(MethodHandles.lookup(), "lambda_forEach", MethodType.methodType(void.class, String.class)); lambda = (Consumer<String>)cs.getTarget().invokeExact(); //end invokedynamic strings.forEach(lambda); }
ご覧のとおり、 LambdaMetafactory
を使用して、ターゲットメソッドのハンドラーを返すファクトリメソッドを提供するCallSiteを作成します。 このメソッドは、 invokeExact
を使用して機能インターフェースの実装を返します。 ラムダ式にキャプチャされた変数がある場合、 invokeExact
はこれらの変数を実際のパラメーターとして受け入れます。
Oracle JRE 8では、メタファクトリーはObjectWeb Asmを使用してJavaクラスを動的に生成します。これにより、機能的なインターフェースを実装するクラスが作成されます。 ラムダ式が外部変数をキャプチャする場合、作成されたクラスに追加のフィールドを追加できます。 これはJavaの匿名クラスに似ていますが、次の違いがあります。
- 匿名クラスは、Javaコンパイラーによって生成されます。
- ラムダ式を実装するためのクラスは、実行時にJVMによって作成されます。
メタファクトリーの実装は、JVMベンダーとバージョンに依存します
もちろん、 invokedynamic
はJavaのラムダ式だけに使用されるわけではありません。 主に、JVM環境で動的言語を実行するときに使用されます。 Javaに組み込まれているNashorn JavaScript エンジンは 、この命令を多用しています。
次に、 LambdaMetafactory
クラスとその機能に注目します。 次へ
この記事のセクションでは、メタファクトリーメソッドの仕組みとMethodHandleとは何かを十分に理解していることを前提としています
ラムダ式のトリック
このセクションでは、日常のタスクで使用する動的なラムダを構築する方法を示します。
チェック済みの例外とラムダ
Javaに存在するすべての機能インターフェースがチェック例外をサポートしないことは秘密ではありません。 通常の例外よりもチェックされた例外の利点は、非常に長年にわたる(そしてまだ熱い)議論です。
しかし、Java Streamsと組み合わせてラムダ式内のチェック済み例外を含むコードを使用する必要がある場合はどうでしょうか。 たとえば、文字列のリストを次のようなURLのリストに変換する必要があります。
Arrays.asList("http://localhost/", "https://github.com").stream() .map(URL::new) .collect(Collectors.toList())
スロー可能な例外はURL(String)のコンストラクターで宣言されているため、 Functiionクラスのメソッド参照として直接使用することはできません。
「いいえ、おそらくこのトリックをここで使用する場合」と言うでしょう。
public static <T> T uncheckCall(Callable<T> callable) { try { return callable.call(); } catch (Exception e) { return sneakyThrow(e); } } private static <E extends Throwable, T> T sneakyThrow0(Throwable t) throws E { throw (E)t; } public static <T> T sneakyThrow(Throwable e) { return Util.<RuntimeException, T>sneakyThrow0(e); } // //return s.filter(a -> uncheckCall(a::isActive)) // .map(Account::getNumber) // .collect(toSet());
これは汚いハックです。 理由は次のとおりです。
- try-catchブロックが使用されます。
- 再び例外がスローされます。
- Javaでの型消去の不正使用。
この問題は、次の事実に関する知識を使用して、より「合法的な」方法で解決できます。
- チェックされた例外は、Javaコンパイラレベルでのみ認識されます。
-
throws
セクションは、JVMレベルでセマンティック値を持たないメソッドの単なるメタデータです。 - チェックされた例外と通常の例外は、JVMのバイトコードレベルでは区別できません。
解決策は、 throws
セクションのないメソッドでCallable.call
メソッドをラップすることです。
static <V> V callUnchecked(Callable<V> callable){ return callable.call(); }
Callable.call
はthrows
セクションでチェック例外を宣言したため、このコードはコンパイルされません。 ただし、動的に構築されたラムダ式を使用してこのセクションを削除できます。
最初に、 throws
セクションを持たない機能インターフェイスを宣言する必要があります。
ただし、 Callable.call
呼び出しを委任できるのは誰Callable.call
:
@FunctionalInterface interface SilentInvoker { MethodType SIGNATURE = MethodType.methodType(Object.class, Callable.class);// INVOKE <V> V invoke(final Callable<V> callable); }
2番目のステップは、 LambdaMetafactory
を使用してこのインターフェイスの実装を作成し、 SilentInvoker.invoke
メソッドのCallable.call
メソッドに委任することです。 前述のように、 throws
セクションはバイトコードレベルでは無視されるため、 SilentInvoker.invoke
メソッドは、例外を宣言せずにCallable.call
メソッドを呼び出すことができます。
private static final SilentInvoker SILENT_INVOKER; final MethodHandles.Lookup lookup = MethodHandles.lookup(); final CallSite site = LambdaMetafactory.metafactory(lookup, "invoke", MethodType.methodType(SilentInvoker.class), SilentInvoker.SIGNATURE, lookup.findVirtual(Callable.class, "call", MethodType.methodType(Object.class)), SilentInvoker.SIGNATURE); SILENT_INVOKER = (SilentInvoker) site.getTarget().invokeExact();
3番目に、例外を宣言せずにCallable.call
を呼び出すヘルパーメソッドを記述します。
public static <V> V callUnchecked(final Callable<V> callable) /*no throws*/ { return SILENT_INVOKER.invoke(callable); }
これで、チェック済み例外の問題なくストリームを書き換えることができます。
Arrays.asList("http://localhost/", "https://dzone.com").stream() .map(url -> callUnchecked(() -> new URL(url))) .collect(Collectors.toList());
callUnchecked
はチェック済み例外を宣言しないため、このコードは問題なくコンパイルされます。 さらに、このメソッドの呼び出しは、 SilentOnvoker
インターフェースを実装するJVM全体の1つのクラスであるため、 SilentOnvoker
インラインキャッシュを使用してインライン化できます。
Callable.call
の実装が実行時に例外をスローした場合、問題なく呼び出し側関数によってCallable.call
されます。
try{ callUnchecked(() -> new URL("Invalid URL")); } catch (final Exception e){ System.out.println(e); }
この方法の可能性にもかかわらず、次の推奨事項を常に覚えておく必要があります。
呼び出されたコードが例外をスローしないことが確実な場合にのみ、callUncheckedでチェック例外を非表示にします
次の例は、このアプローチの例を示しています。
callUnchecked(() -> new URL("https://dzone.com")); // URL MalformedURLException
このメソッドの完全な実装はここにあります 、それはSNAMPオープンソースプロジェクトの一部です。
ゲッターとセッターの使用
このセクションは、JSON、Thriftなどのさまざまなデータ形式のシリアライゼーション/デシリアライゼーションを作成する人に役立ちます。 さらに、JavaBeansのGetterおよびSetterのリフレクションにコードが大きく依存している場合、非常に便利です。
JavaBeanで宣言されたゲッターは、パラメーターを持たず、 void
以外の戻りデータ型を持つgetXXX
というメソッドです。 JavaBeanで宣言されるセッターは、1つのパラメーターを持ち、 void
を返すsetXXX
という名前のメソッドです。 これらの2つの表記は、機能的なインターフェイスとして表すことができます。
- GetterはFunctionクラスで表すことができ、引数は
this
の値です。 - セッターはBiConsumerクラスで表すことができます。最初の引数は
this
で、2番目の引数はSetterに渡される値です。
ここで、ゲッターまたはセッターをこれらに変換できる2つのメソッドを作成します
機能的インターフェース。 そして、両方のインターフェースがジェネリックであることは関係ありません。 タイプを消去した後
実際のデータ型はObject
ます。 戻り値の型と引数の自動キャストは、 LambdaMetafactory
を使用してLambdaMetafactory
できます。 さらに、 Guavaライブラリは、同じゲッターとセッターのラムダ式をキャッシュするのに役立ちます。
最初のステップ:ゲッターとセッターのキャッシュを作成します。 Reflection APIのMethodクラスは、実際のゲッターまたはセッターを表し、キーとして使用されます。
キャッシュ値は、特定のゲッターまたはセッター用の動的に構築された機能インターフェイスです。
private static final Cache<Method, Function> GETTERS = CacheBuilder.newBuilder().weakValues().build(); private static final Cache<Method, BiConsumer> SETTERS = CacheBuilder.newBuilder().weakValues().build();
次に、ゲッターまたはセッターへの参照に基づいて機能インターフェイスのインスタンスを作成するファクトリメソッドを作成します。
private static Function createGetter(final MethodHandles.Lookup lookup, final MethodHandle getter) throws Exception{ final CallSite site = LambdaMetafactory.metafactory(lookup, "apply", MethodType.methodType(Function.class), MethodType.methodType(Object.class, Object.class), //signature of method Function.apply after type erasure getter, getter.type()); //actual signature of getter try { return (Function) site.getTarget().invokeExact(); } catch (final Exception e) { throw e; } catch (final Throwable e) { throw new Error(e); } } private static BiConsumer createSetter(final MethodHandles.Lookup lookup, final MethodHandle setter) throws Exception { final CallSite site = LambdaMetafactory.metafactory(lookup, "accept", MethodType.methodType(BiConsumer.class), MethodType.methodType(void.class, Object.class, Object.class), //signature of method BiConsumer.accept after type erasure setter, setter.type()); //actual signature of setter try { return (BiConsumer) site.getTarget().invokeExact(); } catch (final Exception e) { throw e; } catch (final Throwable e) { throw new Error(e); } }
関数型インターフェイスのObject
型の引数(型消去後)と引数の実際の型と戻り値との間の自動型変換は、 samMethodType
とinstantiatedMethodType
違い(メタファクトリメソッドの3番目と5番目の引数)を使用して実現されます。 インスタンス化されたメソッドのタイプは、ラムダ式の実装を提供するメソッドの特殊化です。
第三に、キャッシングをサポートするこれらの工場のファサードを作成します。
public static Function reflectGetter(final MethodHandles.Lookup lookup, final Method getter) throws ReflectiveOperationException { try { return GETTERS.get(getter, () -> createGetter(lookup, lookup.unreflect(getter))); } catch (final ExecutionException e) { throw new ReflectiveOperationException(e.getCause()); } } public static BiConsumer reflectSetter(final MethodHandles.Lookup lookup, final Method setter) throws ReflectiveOperationException { try { return SETTERS.get(setter, () -> createSetter(lookup, lookup.unreflect(setter))); } catch (final ExecutionException e) { throw new ReflectiveOperationException(e.getCause()); } }
Java Reflection APIを使用してMethod
クラスのインスタンスから取得したメソッド情報は、 MethodHandle
簡単に変換できます。 クラスインスタンスメソッドには、このメソッドにこれを渡すthis
に使用される隠された最初の引数が常にあることに注意してください。 静的メソッドには、このようなパラメーターはありません。 たとえば、 Integer.intValue()
メソッドの実際の署名はint intValue(Integer this)
ように見えます。 このトリックは、ゲッターとセッターの機能ラッパーの実装で使用されます。
次に、コードをテストします。
final Date d = new Date(); final BiConsumer<Date, Long> timeSetter = reflectSetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("setTime", long.class)); timeSetter.accept(d, 42L); //the same as d.setTime(42L); final Function<Date, Long> timeGetter = reflectGetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("getTime")); System.out.println(timeGetter.apply(d)); //the same as d.getTime() //output is 42
キャッシュされたゲッターおよびセッターを使用したこのアプローチは、シリアライズおよびデシリアライズ中にゲッターおよびセッターを使用するシリアライゼーション/デシリアライゼーションライブラリ(ジャクソンなど)で効果的に使用できます。
LambdaMetaFactory
を使用して動的に生成された実装で機能インターフェースを呼び出すことは、Java Reflection APIを介して呼び出すよりもはるかに高速です。
コードの完全版はここにあり、 SNAMPライブラリの一部です。
制限とバグ
このセクションでは、JavaコンパイラとJVMのラムダ式に関連するいくつかのバグと制限について説明します。 これらの制限はすべて、WindowsおよびLinux用のjavac
バージョン1.8.0_131を使用したOpenJDKおよびOracle JDKで再現できます。
メソッドハンドラーからラムダ式を作成する
ご存知のように、ラムダ式はLambdaMetaFactory
を使用して動的に構築できます。 これを行うには、ハンドラー( MethodHandle
クラス)を定義する必要があります。これは、機能インターフェースで定義されている唯一のメソッドの実装を示します。 この簡単な例を見てみましょう。
final class TestClass { String value = ""; public String getValue() { return value; } public void setValue(final String value) { this.value = value; } } final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final MethodHandles.Lookup lookup = MethodHandles.lookup(); final CallSite site = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, TestClass.class), MethodType.methodType(Object.class), lookup.findVirtual(TestClass.class, "getValue", MethodType.methodType(String.class)), MethodType.methodType(String.class)); final Supplier<String> getter = (Supplier<String>) site.getTarget().invokeExact(obj); System.out.println(getter.get());
このコードは次と同等です:
final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final Supplier<String> elementGetter = () -> obj.getValue(); System.out.println(elementGetter.get());
しかし、 getValue
を指すメソッドハンドラーをゲッターフィールドが表すハンドラーで置き換えるとどうなるでしょうか。
final CallSite site = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, TestClass.class), MethodType.methodType(Object.class), lookup.findGetter(TestClass.class, "value", String.class), //field getter instead of method handle to getValue MethodType.methodType(String.class));
findGetter
がゲッターフィールドをポイントし、正しい署名を持つハンドラーを返すため、このコードは予想どおりに機能するはずです。 ただし、このコードを実行すると、次の例外が表示されます。
java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: getField
興味深いことに、 MethodHandleProxiesを使用すると、フィールドのゲッターは問題なく機能します。
final Supplier<String> getter = MethodHandleProxies .asInterfaceInstance(Supplier.class, lookup.findGetter(TestClass.class, "value", String.class) .bindTo(obj));
MethodHandleProxies
は、ラムダ式を動的に作成する良い方法ではないことに注意してください。このクラスは、単にMethodHandle
をプロキシクラスでラップし、invocationHandler.invokeをMethodHandle.invokeWithArgumentsに委任するためです 。 このアプローチはJava Reflectionを使用し、非常に時間がかかります。
前に示したように、すべてのメソッドハンドラーを使用して実行時にラムダ式を作成できるわけではありません。
ラムダ式を動的に作成するために使用できるメソッドハンドラは数種類のみです。
ここにあります:
- REF_invokeInterface:インターフェイスメソッドのLookup.findVirtualを使用して作成できます
- REF_invokeVirtual:クラス仮想メソッドのLookup.findVirtualを使用して作成できます
- REF_invokeStatic:静的メソッドのLookup.findStaticを使用して作成
- REF_newInvokeSpecial:コンストラクターのLookup.findConstructorを使用して作成できます
- REF_invokeSpecial:Lookup.findSpecialを使用して作成できます
プライベートメソッドおよびクラス仮想メソッドを使用した事前バインディング
他のタイプのハンドラーはLambdaConversionException
エラーをLambdaConversionException
ます。
一般的な例外
このバグは、Javaコンパイラと、 throws
セクションで一般的な例外を宣言する機能に関連しています。 次のコード例は、この動作を示しています。
interface ExtendedCallable<V, E extends Exception> extends Callable<V>{ @Override V call() throws E; } final ExtendedCallable<URL, MalformedURLException> urlFactory = () -> new URL("http://localhost"); urlFactory.call();
URL
クラスのコンストラクターがMalformedURLException
スローするため、このコードをコンパイルする必要があります。 しかし、コンパイルはしません。 次のエラーメッセージが表示されます。
Error:(46, 73) java: call() in <anonymous Test$CODEgt; cannot implement call() in ExtendedCallable overridden method does not throw java.lang.Exception
ただし、ラムダ式を匿名クラスに置き換えると、コードはコンパイルされます。
final ExtendedCallable<URL, MalformedURLException> urlFactory = new ExtendedCallable<URL, MalformedURLException>() { @Override public URL call() throws MalformedURLException { return new URL("http://localhost"); } }; urlFactory.call();
これは次のとおりです。
一般的な例外の型推論は、ラムダ式と組み合わせて正しく機能しません
パラメータ化タイプの制限
&
: <T extends A & B & C & ... Z>
記号を使用して、いくつかの型制限を持つ汎用オブジェクトを構築できます。
ジェネリックパラメーターを決定するこの方法はめったに使用されませんが、いくつかの制限があるため、特定の方法でJavaのラムダ式に影響します。
- 最初のタイプを除く各タイプの制約は、インターフェースでなければなりません。
- このようなジェネリックを持つクラスの純粋なバージョンでは、リストの最初のタイプの制約のみが考慮されます。
2番目の制限により、コンパイル時と実行時にラムダ式へのリンクがある場合、コードの動作が異なります。 この違いは、次のコードを使用して実証できます。
final class MutableInteger extends Number implements IntSupplier, IntConsumer { //mutable container of int value private int value; public MutableInteger(final int v) { value = v; } @Override public int intValue() { return value; } @Override public long longValue() { return value; } @Override public float floatValue() { return value; } @Override public double doubleValue() { return value; } @Override public int getAsInt() { return intValue(); } @Override public void accept(final int value) { this.value = value; } } static <T extends Number & IntSupplier> OptionalInt findMinValue(final Collection <T> values) { return values.stream().mapToInt(IntSupplier::getAsInt).min(); } final List <MutableInteger> values = Arrays.asList(new MutableInteger(10), new MutableInteger(20)); final int mv = findMinValue(values).orElse(Integer.MIN_VALUE); System.out.println(mv);
このコードは完全に正しく、正常にコンパイルされます。 MutableInteger
クラスは、ジェネリック型Tの制限を満たします。
-
MutableInteger
はNumber
継承します。 -
MutableInteger
実装しIntSupplier
。
ただし、実行時に例外が発生してコードがクラッシュします。
java.lang.BootstrapMethodError: call site initialization exception at java.lang.invoke.CallSite.makeSite(CallSite.java:341) at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307) at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297) at Test.minValue(Test.java:77) Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type class java.lang.Number; not a subtype of implementation type interface java.util.function.IntSupplier at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233) at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303) at java.lang.invoke.CallSite.makeSite(CallSite.java:302)
これは、JavaStreamパイプラインが純粋な型(この場合はNumber
クラス)のみをキャプチャし、 IntSupplier
インターフェイスを実装しないためにIntSupplier
ます。 この問題は、メソッドへの参照として使用される別のメソッドでパラメーターの型を明示的に宣言することで修正できます。
private static int getInt(final IntSupplier i){ return i.getAsInt(); } private static <T extends Number & IntSupplier> OptionalInt findMinValue(final Collection<T> values){ return values.stream().mapToInt(UtilsTest::getInt).min(); }
この例は、コンパイラとランタイムでの不正な型推論を示しています。
コンパイル時および実行時にラムダ式を使用することと組み合わされた汎用パラメータタイプのいくつかの制約の処理には一貫性がありません