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

この䟋は、コンパむラずランタむムでの䞍正な型掚論を瀺しおいたす。




コンパむル時および実行時にラムダ匏を䜿甚するこずず組み合わされた汎甚パラメヌタタむプのいく぀かの制玄の凊理には䞀貫性がありたせん





Source: https://habr.com/ru/post/J432418/


All Articles