Javaでのラムダ式の解析

画像







翻訳者から: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の匿名クラスに似ていますが、次の違いがあります。












メタファクトリーの実装は、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());
      
      





これは汚いハックです。 理由は次のとおりです。









この問題は、次の事実に関する知識を使用して、より「合法的な」方法で解決できます。









解決策は、 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つの表記は、機能的なインターフェイスとして表すことができます。









ここで、ゲッターまたはセッターをこれらに変換できる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を使用し、非常に時間がかかります。







前に示したように、すべてのメソッドハンドラーを使用して実行時にラムダ式を作成できるわけではありません。










ラムダ式を動的に作成するために使用できるメソッドハンドラは数種類のみです。










ここにあります:









他のタイプのハンドラーは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の制限を満たします。









ただし、実行時に例外が発生してコードがクラッシュします。







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





この例は、コンパイラとランタイムでの不正な型推論を示しています。










コンパイル時および実行時にラムダ式を使用することと組み合わされた汎用パラメータタイプのいくつかの制約の処理には一貫性がありません











All Articles