
من أحد المترجمين: ربما تكون 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) {
يمكن invokedynamic
التعليمة invokedynamic
تقريبًا على أنها كود Java:
private static CallSite cs; void printElements(List<String> strings) { Consumer<String> 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); }
هذا هو الاختراق القذر. وهنا السبب:
- يتم استخدام كتلة محاولة الصيد.
- يتم طرح الاستثناء مرة أخرى.
- الاستخدام القذر لنوع المحو في جافا.
يمكن حل المشكلة بطريقة أكثر "قانونية" ، وذلك باستخدام معرفة الحقائق التالية:
- يتم التعرف على الاستثناءات المحددة فقط على مستوى برنامج التحويل البرمجي لـ 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);
تتمثل الخطوة الثانية في إنشاء تطبيق لهذه الواجهة باستخدام 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) { 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"));
التنفيذ الكامل لهذه الطريقة موجود هنا ، فهو جزء من مشروع 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),
يتم تحقيق التحويل التلقائي للنوع بين وسيطات النوع 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);
يمكن استخدام هذا الأسلوب مع 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),
يجب أن يعمل هذا الرمز ، كما هو متوقع ، لأن 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 {
هذا الرمز هو الصحيح تماما ويجمع بنجاح. 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 في وقت الترجمة وفي وقت التشغيل غير ثابت