تحليل تعبيرات lambda في Java

الصورة


من أحد المترجمين: ربما تكون LambdaMetafactory واحدة من أكثر آليات Java 8. التي تم الاستخفاف بها ، وقد اكتشفناها مؤخرًا ، لكننا نقدر قدراتها بالفعل. يعمل الإصدار 7.0 من إطار CUBA على تحسين الأداء عن طريق تجنب المكالمات العاكسة لصالح إنشاء تعبيرات lambda. أحد تطبيقات هذه الآلية في إطار عملنا هو ربط معالجات أحداث التطبيق بالتعليقات التوضيحية ، وهي مهمة شائعة ، وهي تناظر EventListener من Spring. نعتقد أن المعرفة بمبادئ LambdaFactory يمكن أن تكون مفيدة في العديد من تطبيقات Java ، ونسرع في مشاركة هذه الترجمة معك.


في هذه المقالة ، سنعرض بعض الحيل غير المعروفة عند استخدام تعبيرات lambda في Java 8 وحدود هذه التعبيرات. الجمهور المستهدف للمقال هو كبار مطوري جافا والباحثين ومطوري مجموعة الأدوات. سيتم استخدام واجهة برمجة تطبيقات Java العامة فقط دون com.sun.* والفئات الداخلية الأخرى ، وبالتالي فإن الكود محمول بين تطبيقات JVM المختلفة.


مقدمة قصيرة


ظهرت تعبيرات Lambda في Java 8 كوسيلة لتطبيق أساليب مجهولة و
في بعض الحالات ، كبديل للفصول المجهولة. على مستوى bytecode ، يتم استبدال تعبير lambda invokedynamic . يتم استخدام هذا الإرشادات لإنشاء تطبيق واجهة وظيفية وطريقة فقط تفويض استدعاء الأسلوب الفعلي ، الذي يحتوي على التعليمات البرمجية المعرفة في نص التعبير lambda.


على سبيل المثال ، لدينا الكود التالي:


 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 التعليمة 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 . إذا كانت هناك متغيرات تم التقاطها في تعبير lambda ، فإن invokeExact يقبل هذه المتغيرات كمعلمات فعلية.


في Oracle JRE 8 ، يولد metafactory ديناميكيًا فئة Java باستخدام ObjectWeb Asm ، والذي ينشئ فئة تنفذ واجهة وظيفية. يمكن إضافة حقول إضافية إلى الفئة التي تم إنشاؤها إذا كان التعبير lambda يلتقط المتغيرات الخارجية. يشبه هذا أحد فئات Java المجهولة ، ولكن هناك الاختلافات التالية:


  • يتم إنشاء فئة مجهولة بواسطة برنامج التحويل البرمجي Java.
  • يتم إنشاء فئة لتطبيق تعبير lambda بواسطة JVM في وقت التشغيل.



يعتمد تطبيق Metafactory على بائع JVM وإصداره




بالطبع ، لا يتم استخدام invokedynamic فقط لتعبيرات lambda في Java. يستخدم بشكل رئيسي عند تشغيل اللغات الديناميكية في بيئة JVM. يستفيد محرك Nashorn JavaScript ، المدمج في Java ، من هذه التعليمات.


بعد ذلك ، سوف نركز على فئة LambdaMetafactory وقدراتها. التالي
يفترض قسم هذه المقالة أنك تفهم جيدًا كيف تعمل طرق MethodHandle وما MethodHandle


الحيل مع تعبيرات امدا


في هذا القسم ، سنعرض كيفية بناء لامبدا ديناميكية لاستخدامها في المهام اليومية.


استثناءات فحص و lambdas


ليس سراً أن جميع الواجهات الوظيفية الموجودة في Java لا تدعم الاستثناءات المحددة. مزايا الاستثناءات المحددة على الاستثناءات العادية هي نقاش طويل جدًا (وما زال ساخنًا).


ولكن ماذا لو كنت بحاجة إلى استخدام الكود مع استثناءات محددة داخل تعبيرات lambda بالاقتران مع Java Streams؟ على سبيل المثال ، تحتاج إلى تحويل قائمة سلاسل إلى قائمة عناوين URL مثل:


 Arrays.asList("http://localhost/", "https://github.com").stream() .map(URL::new) .collect(Collectors.toList()) 

يتم الإعلان عن استثناء قابل للقذف في مُنشئ عنوان URL (سلسلة) ، لذلك لا يمكن استخدامه مباشرة كمرجع طريقة في فئة 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()); 

هذا هو الاختراق القذر. وهنا السبب:


  • يتم استخدام كتلة محاولة الصيد.
  • يتم طرح الاستثناء مرة أخرى.
  • الاستخدام القذر لنوع المحو في جافا.

يمكن حل المشكلة بطريقة أكثر "قانونية" ، وذلك باستخدام معرفة الحقائق التالية:


  • يتم التعرف على الاستثناءات المحددة فقط على مستوى برنامج التحويل البرمجي لـ Java.
  • قسم throws هو مجرد بيانات وصفية لطريقة بدون قيمة دلالية على مستوى JVM.
  • لا يمكن تمييز الاستثناءات المحددة والمعتادة على مستوى الكود في JVM.

الحل هو التفاف الأسلوب Callable.call بأسلوب بدون قسم throws :


 static <V> V callUnchecked(Callable<V> callable){ return callable.call(); } 

لا يتم ترجمة هذا الرمز لأن أسلوب Callable.call أعلن استثناءات محددة في قسم throws . ولكن يمكننا إزالة هذا القسم باستخدام تعبير lambda مبني ديناميكيًا.


أولاً نحتاج إلى إعلان واجهة وظيفية لا تحتوي على قسم throws .
ولكن من سيكون قادرًا على تفويض المكالمة إلى Callable.call :


 @FunctionalInterface interface SilentInvoker { MethodType SIGNATURE = MethodType.methodType(Object.class, Callable.class);//  INVOKE <V> V invoke(final Callable<V> callable); } 

تتمثل الخطوة الثانية في إنشاء تطبيق لهذه الواجهة باستخدام 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(); 

ثالثًا ، نكتب طريقة مساعدة تستدعي 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 لا تعلن الاستثناءات المحددة. علاوة على ذلك ، يمكن استدعاء هذه الطريقة باستخدام ذاكرة التخزين المؤقت المضمنة في الشكل ، لأنها فئة واحدة فقط في JVM بأكملها تنفذ واجهة 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 مفتوح المصدر.


العمل مع Getters و Setters


سيكون هذا القسم مفيدًا لأولئك الذين يكتبون التسلسل / إلغاء التسلسل لمختلف تنسيقات البيانات مثل JSON و Thrift وما إلى ذلك. علاوة على ذلك ، يمكن أن يكون مفيدًا للغاية إذا كانت الشفرة تعتمد اعتمادًا كبيرًا على انعكاس Getters and Setters في JavaBeans.


إن getter المصرح به في JavaBean هي طريقة تسمى getXXX بدون أي معلمات ونوع بيانات إرجاع غير void . يعد setter الذي تم تعريفه في JavaBean طريقة تسمى setXXX ، مع معلمة واحدة وإرجاع void . يمكن تمثيل هذين الترميزين كواجهات وظيفية:


  • يمكن تمثيل Getter بواسطة فئة الدالة ، حيث تكون الوسيطة هي قيمة this .
  • يمكن تمثيل Setter بواسطة فئة BiConsumer ، حيث تكون الوسيطة الأولى هي this ، والثاني هو القيمة التي يتم تمريرها إلى Setter.

الآن سنقوم بإنشاء طريقتين يمكنهما تحويل أي getter أو setter إلى هذه
واجهات وظيفية. وليس من المهم أن تكون كلتا الواجهتين من الأدوية العامة. بعد محو الأنواع
سيكون نوع البيانات الحقيقي هو Object . يمكن إجراء الصب التلقائي لنوع الإرجاع والحجج باستخدام LambdaMetafactory . بالإضافة إلى ذلك ، ستساعد مكتبة Guava في تخزين تعبيرات lambda للتخزين المؤقت لنفس الحروف والمستوطنين.


الخطوة الأولى: إنشاء ذاكرة تخزين مؤقت للألواح والأدوات. تمثل فئة الطريقة لواجهة برمجة التطبيقات Reflection API أو أداة ضبط حقيقية وتستخدم كمفتاح.
قيمة ذاكرة التخزين المؤقت هي واجهة وظيفية تم إنشاؤها ديناميكيًا لجهاز محدد أو محدد.


 private static final Cache<Method, Function> GETTERS = CacheBuilder.newBuilder().weakValues().build(); private static final Cache<Method, BiConsumer> SETTERS = CacheBuilder.newBuilder().weakValues().build(); 

ثانياً ، سنقوم بإنشاء أساليب المصنع التي تنشئ مثيلًا للواجهة الوظيفية استنادًا إلى المراجع إلى getter أو setter.


 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 وقيمة الإرجاع باستخدام الفرق بين samMethodType و instantiatedMethodType ( samMethodType الثالثة والخامسة لطريقة metafactory ، على التوالي). نوع المثيل الذي تم إنشاؤه للطريقة - وهذا هو تخصص الطريقة التي توفر تنفيذ تعبير lambda.


ثالثًا ، سننشئ واجهة لهذه المصانع مع دعم للتخزين المؤقت:


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

يمكن بسهولة تحويل معلومات الطريقة التي تم الحصول عليها من مثيل لفئة Method باستخدام Java Reflection API إلى 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 

يمكن استخدام هذا الأسلوب مع getters المخزنة مؤقتاً و setters بشكل فعال في مكتبات التسلسل / إلغاء التسلسل (مثل Jackson) التي تستخدم getters و setters أثناء التسلسل و إلغاء التسلسل.




استدعاء واجهات وظيفية مع تطبيقات تم إنشاؤها ديناميكيًا باستخدام LambdaMetaFactory أسرع بكثير من الاتصال من خلال Java Reflection API




يمكن العثور على النسخة الكاملة من الكود هنا ، وهي جزء من مكتبة SNAMP .


القيود والبق


في هذا القسم ، سننظر في بعض الأخطاء والقيود المرتبطة بتعبيرات lambda في برنامج التحويل البرمجي لـ Java و JVM. كل هذه القيود يمكن استنساخها في OpenJDK و Oracle JDK مع javac الإصدار 1.8.0_131 لنظامي Windows و Linux.


إنشاء تعبيرات lambda من معالجات الطريقة


كما تعلمون ، يمكن إنشاء تعبير lambda ديناميكيًا باستخدام 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 الذي تمثله حقول getter:


 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 بإرجاع معالج يشير إلى حقول getter ولديه التوقيع الصحيح. ولكن إذا قمت بتشغيل هذا الرمز ، فسترى الاستثناء التالي:


 java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: getField 

ومن المثير للاهتمام ، أن getter لهذا الحقل يعمل بشكل جيد إذا استخدمنا MethodHandleProxies :


 final Supplier<String> getter = MethodHandleProxies .asInterfaceInstance(Supplier.class, lookup.findGetter(TestClass.class, "value", String.class) .bindTo(obj)); 

تجدر الإشارة إلى أن MethodHandleProxies ليست طريقة جيدة لإنشاء تعبيرات lambda بشكل حيوي ، لأن هذه الفئة ببساطة التفاف MethodHandle في فئة وكيل وتفويض invocationHandler.invoke إلى MethodHandle.invokeWithArguments . يستخدم هذا النهج Java Reflection وهو بطيء جدًا.


كما هو موضح سابقًا ، لا يمكن استخدام كل معالجات الطريقة لإنشاء تعبيرات lambda في وقت التشغيل.




يمكن استخدام أنواع قليلة فقط من معالجات الطريقة لإنشاء تعبيرات lambda ديناميكيًا.




ها هم:



سوف أنواع أخرى من معالجات 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 

ولكن ، إذا استبدلنا تعبير lambda بفصل مجهول ، فستجمع الشفرة:


 final ExtendedCallable<URL, MalformedURLException> urlFactory = new ExtendedCallable<URL, MalformedURLException>() { @Override public URL call() throws MalformedURLException { return new URL("http://localhost"); } }; urlFactory.call(); 

يتبع من هذا:




لا يعمل الاستنتاج النوعي للاستثناءات العامة بشكل صحيح مع تعبيرات lambda




قيود نوع المعلمة


يمكنك إنشاء كائن عام مع العديد من قيود الكتابة باستخدام علامة & : <T extends A & B & C & ... Z> .
نادرًا ما يتم استخدام هذه الطريقة لتحديد المعلمات العامة ، ولكن بطريقة ما تؤثر على تعبيرات lambda في Java بسبب بعض القيود:


  • يجب أن يكون كل نوع قيد ، باستثناء الأول ، واجهة.
  • يأخذ إصدار خالص من فئة مع هذا النوع العام في الاعتبار فقط قيد النوع الأول من القائمة.

يؤدي القيد الثاني إلى سلوكيات مختلفة من التعليمات البرمجية في وقت التحويل البرمجي وفي وقت التشغيل ، عندما يحدث الربط بتعبير lambda. يمكن توضيح هذا الاختلاف باستخدام الكود التالي:


 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 فئة 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 لا يلتقط إلا نوعًا خالصًا ، وهو في حالتنا فئة الفئة ولا يطبق واجهة 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(); } 

يوضح هذا المثال استنتاج النوع غير الصحيح في المحول البرمجي ووقت التشغيل.




معالجة قيود نوع المعلمة العامة متعددة بالاقتران مع استخدام تعبيرات lambda في وقت الترجمة وفي وقت التشغيل غير ثابت



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


All Articles