لقد وصل Java 8، وحان الوقت لتعلم شيء جديد. Java 7 وJava 6 هما مجرد نسختين معدلتين قليلاً، لكن Java 8 ستحتوي على تحسينات كبيرة. ربما Java 8 كبير جدًا؟ سأقدم لك اليوم شرحًا شاملاً للتجريد الجديد CompletableFuture في JDK 8. كما نعلم جميعًا، سيتم إصدار Java 8 في أقل من عام، لذلك تعتمد هذه المقالة على JDK 8 build 88 مع دعم lambda. يمتد برنامج CompletableFuture إلى المستقبل ويوفر الأساليب والمشغلين الأحاديين ويعزز عدم التزامن ونموذج البرمجة المبني على الأحداث والذي لا يتوقف مع الإصدارات القديمة من Java. إذا قمت بفتح JavaDoc الخاص بـ CompletableFuture فسوف تصاب بالصدمة. هناك حوالي خمسين طريقة (!)، وبعضها مثير للاهتمام للغاية ويصعب فهمه، على سبيل المثال:
انسخ الكود كما يلي: public <U,V> CompletableFuture<V> ثمCombineAsync(
المستقبل الكامل <؟ يمتد U> آخر،
وظيفة ثنائية<؟ سوبر تي، سوبر يو،؟
المنفذ المنفذ)
لا تقلق، استمر في القراءة. يقوم CompletableFuture بجمع كافة خصائص الاستماع إلى المستقبل في الجوافة وSettableFuture. بالإضافة إلى ذلك، فإن تعبيرات لامدا المضمنة تجعلها أقرب إلى العقود الآجلة لـ Scala/Akka. قد يبدو هذا جيدًا جدًا لدرجة يصعب تصديقها، لكن واصل القراءة. يحتوي CompletableFuture على جانبين رئيسيين يتفوقان على رد الاتصال/التحويل غير المتزامن الخاص بـ Future في ol، والذي يسمح بتعيين قيمة CompletableFuture من أي مؤشر ترابط في أي وقت.
1. استخراج وتعديل قيمة الحزمة
غالبًا ما تمثل العقود الآجلة تعليمات برمجية يتم تشغيلها في سلاسل رسائل أخرى، ولكن هذا ليس هو الحال دائمًا. في بعض الأحيان تريد إنشاء مستقبل للإشارة إلى أنك تعرف ما سيحدث، مثل وصول رسالة JMS. لذا، لديك مستقبل ولكن لا يوجد عمل غير متزامن محتمل في المستقبل. أنت فقط تريد أن تنتهي (تحل) ببساطة عند وصول رسالة JMS مستقبلية، والتي تكون مدفوعة بحدث ما. في هذه الحالة، يمكنك ببساطة إنشاء CompletableFuture للعودة إلى عميلك، وببساطة سوف يقوم Complete() بفتح جميع العملاء الذين ينتظرون المستقبل طالما كنت تعتقد أن نتيجتك متاحة.
أولاً، يمكنك ببساطة إنشاء CompletableFuture جديد وإعطائه لعميلك:
انسخ الكود كما يلي: public CompletableFuture<String> Ask() {
Final CompletableFuture<String> Future = new CompletableFuture<>();
//...
مستقبل العودة؛
}
لاحظ أن هذا المستقبل ليس له أي اتصال بـ Callable، ولا يوجد تجمع مؤشرات ترابط، ولا يعمل بشكل غير متزامن. إذا كان رمز العميل يتصل الآن بـ Ask().get() فسيتم حظره إلى الأبد. إذا أكملت السجلات رد الاتصال، فلن تصبح نافذة المفعول أبدًا. إذن ما هو المفتاح؟ الآن يمكنك أن تقول:
انسخ الكود كما يلي: Future.Complete("42")
...في هذه اللحظة، سيحصل جميع عملاء Future.get() على نتيجة السلسلة، وسيصبح ساري المفعول فورًا بعد إكمال رد الاتصال. يعد هذا مناسبًا جدًا عندما تريد تمثيل مهمة مستقبلية، وليست هناك حاجة لحساب مهمة بعض سلاسل التنفيذ. لا يمكن استدعاء CompletableFuture.complete() إلا مرة واحدة، وسيتم تجاهل الاستدعاءات اللاحقة. ولكن هناك أيضًا باب خلفي يسمى CompletableFuture.obtrudeValue(...) يقوم بالكتابة فوق القيمة السابقة لمستقبل جديد، لذا يرجى استخدامه بحذر.
في بعض الأحيان تريد أن ترى ما يحدث عندما تفشل الإشارة، كما تعلم يمكن للكائن المستقبلي التعامل مع النتيجة أو الاستثناء الذي يحتوي عليه. إذا كنت تريد تمرير بعض الاستثناءات بشكل أكبر، فيمكنك استخدام CompletableFuture.completeExceptionally(ex) (أو استخدم طريقة أكثر قوة مثل obtrudeException(ex) لتجاوز الاستثناء السابق). يقوم التابع CompleteExceptionally() أيضًا بفتح جميع العملاء المنتظرين، ولكن هذه المرة يطرح استثناءً من get(). بالحديث عن get()، هناك أيضًا طريقة CompletableFuture.join() مع تغييرات طفيفة في معالجة الأخطاء. لكن بشكل عام كلهم نفس الشيء أخيرًا، هناك طريقة CompletableFuture.getNow(valueIfAbsent) التي لا تحظر ولكنها ستعيد القيمة الافتراضية إذا لم يكتمل المستقبل بعد، مما يجعلها مفيدة جدًا عند بناء أنظمة قوية حيث لا نريد الانتظار لفترة طويلة.
الطريقة الثابتة النهائية هي استخدام CompleteFuture(value) لإرجاع كائن Future المكتمل، والذي قد يكون مفيدًا جدًا عند اختبار أو كتابة بعض طبقات المحول.
2. قم بإنشاء واحصل على CompleteableFuture
حسنًا، هل إنشاء CompletableFuture يدويًا هو خيارنا الوحيد؟ غير مؤكد. تمامًا مثل العقود الآجلة العادية، يمكننا ربط المهام الموجودة، ويستخدم CompletableFuture أساليب المصنع:
انسخ رمز الكود كما يلي:
static <U> CompletableFuture<U> SupplyAsync(Supplier<U> المورد);
static <U> CompletableFuture<U> SupplyAsync(Supplier<U> المورد، المنفذ المنفذ)؛
static CompletableFuture<Void> runAsync(Runnable runnable);
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);
ينتهي مُنفِّذ الطريقة بدون معلمات بـ...Async وسيستخدم ForkJoinPool.commonPool() (تجمع عالمي مشترك تم تقديمه في JDK8)، والذي ينطبق على معظم الأساليب في فئة CompletableFuture. من السهل فهم runAsync()، لاحظ أنه يتطلب Runnable، لذا فهو يُرجع CompletableFuture<Void> لأن Runnable لا يُرجع أي قيمة. إذا كنت بحاجة إلى التعامل مع العمليات غير المتزامنة وإرجاع النتائج، استخدم المورد<U>:
انسخ رمز الكود كما يلي:
Final CompletableFuture<String> Future = CompletableFuture.supplyAsync(new المورد<String>() {
@تجاوز
الحصول على سلسلة عامة () {
//...طويلة...
إرجاع "42" ؛
}
}، المنفذ)؛
لكن لا تنس أن هناك تعبيرات lambdas في Java 8!
انسخ رمز الكود كما يلي:
FinalCompletableFuture<String> Future = CompletableFuture.supplyAsync(() -> {
//...طويلة...
إرجاع "42" ؛
}، المنفذ)؛
أو:
انسخ رمز الكود كما يلي:
Final CompletableFuture<String> Future =
CompletableFuture.supplyAsync(() -> longRunningTask(params), executor);
على الرغم من أن هذه المقالة لا تتعلق بلامدا، إلا أنني أستخدم تعبيرات لامدا بشكل متكرر.
3. التحويل والإجراء على CompletableFuture (ثم قم بالتطبيق)
لقد قلت أن CompleteableFuture أفضل من Future لكن لا تعرف لماذا؟ ببساطة، لأن CompletableFuture هو ذرة وعامل. أليس ما قلته مفيدا؟ يسمح لك كل من Scala وJavaScript بتسجيل رد اتصال غير متزامن عند اكتمال المستقبل، ولا يتعين علينا الانتظار وحظره حتى يصبح جاهزًا. يمكننا أن نقول ببساطة: عند تشغيل هذه الوظيفة، تظهر النتيجة. بالإضافة إلى ذلك، يمكننا تكديس هذه الوظائف، والجمع بين العقود الآجلة المتعددة معًا، وما إلى ذلك. على سبيل المثال، إذا قمنا بالتحويل من سلسلة إلى عدد صحيح، فيمكننا التحويل من CompletableFuture إلى CompletableFuture<Integer بدون اقتران. يتم ذلك عبر تطبيقthenApply():
انسخ رمز الكود كما يلي:
<U> CompletableFuture<U>thenApply(Function<? super T,? Extends U> fn);
<U> CompletableFuture<U>thenApplyAsync(Function<? super T,? Extends U> fn);
<U> CompletableFuture<U> ثمApplyAsync(Function<? super T,? Extends U> fn, Executor executor);<p></p>
<p>كما ذكرنا... يوفر الإصدار Async معظم العمليات على CompletableFuture، لذا سأتخطاها في الأقسام اللاحقة. تذكر أن الطريقة الأولى ستستدعي الطريقة الموجودة في نفس مؤشر الترابط الذي اكتمل فيه المستقبل، بينما ستستدعيها الطريقتان المتبقيتان بشكل غير متزامن في تجمعات سلاسل رسائل مختلفة.
دعونا نلقي نظرة على سير العمل لـthenApply():</p>
<p><قبل>
CompletableFuture<String> f1 = //...
CompletableFuture<Integer> f2 = f1.thenApply(Integer::parseInt);
CompletableFuture<Double> f3 = f2.thenApply(r -> r * r * Math.PI);
</ص>
أو في بيان:
انسخ رمز الكود كما يلي:
CompletableFuture<Double> f3 =
f1.thenApply(Integer::parseInt).thenApply(r -> r * r * Math.PI);
هنا، سترى تحويل التسلسل، من String إلى Integer إلى Double. ولكن الأهم من ذلك أن هذه التحولات لا تنفذ على الفور ولا تتوقف. هذه التحولات لا تنفذ على الفور ولا تتوقف. إنهم ببساطة يتذكرون البرنامج الذي قاموا بتنفيذه عند اكتمال الإصدار f1 الأصلي. إذا كانت بعض التحويلات تستغرق وقتًا طويلاً جدًا، فيمكنك توفير المنفذ الخاص بك لتشغيلها بشكل غير متزامن. لاحظ أن هذه العملية تعادل خريطة أحادية في سكالا.
4. قم بتشغيل الكود المكتمل (ثم قبول/ثم تشغيل)
انسخ رمز الكود كما يلي:
CompletableFuture<Void>thenAccept(Consumer<? super T> block);
CompletableFuture<Void> ثمRun(إجراء قابل للتشغيل);
هناك طريقتان نموذجيتان للمرحلة "النهائية" في خطوط الأنابيب المستقبلية. يتم إعدادها عند استخدام القيمة المستقبلية. عندما توفرthenAccept() القيمة النهائية، يقوم Runn بتنفيذ Runnable، الذي لا يحتوي حتى على طريقة لحساب القيمة. على سبيل المثال:
انسخ رمز الكود كما يلي:
Future.thenAcceptAsync(dbl -> log.debug("النتيجة: {}"، dbl)، executor);
log.debug("مستمر");
...المتغيرات غير المتزامنة متاحة أيضًا بطريقتين، المنفذين الضمنيين والصريحين، ولن أؤكد على هذه الطريقة كثيرًا.
لا يتم حظر أساليبthenAccept()/thenRun() (حتى لو لم يكن هناك منفذ صريح). إنهم يشبهون مستمع/معالج الأحداث، والذي سيتم تنفيذه لفترة من الوقت عند توصيله بالمستقبل. ستظهر رسالة "مستمر" على الفور، على الرغم من أن المستقبل لم يكتمل بعد.
5. خطأ في التعامل مع CompletableFuture واحد
حتى الآن، ناقشنا فقط نتائج الحسابات. ماذا عن الاستثناءات؟ هل يمكننا التعامل معها بشكل غير متزامن؟ بالتأكيد!
انسخ رمز الكود كما يلي:
CompletableFuture<String> آمن =
Future.exceptionally(ex -> "لدينا مشكلة:" + ex.getMessage());
عندما يقبل Exception() دالة، سيتم استدعاء المستقبل الأصلي لطرح استثناء. ستتاح لنا الفرصة لتحويل هذا الاستثناء إلى قيمة متوافقة مع النوع المستقبلي لاسترداده. لن تؤدي التحويلات الآمنة الإضافية إلى ظهور استثناءات بعد الآن ولكنها ستعيد بدلاً من ذلك قيمة سلسلة من الوظيفة التي توفر الوظيفة.
الطريقة الأكثر مرونة هي أن يقبل Handle() دالة تتلقى النتيجة أو الاستثناء الصحيح:
انسخ رمز الكود كما يلي:
CompletableFuture<Integer> Safe = Future.handle((ok, ex) -> {
إذا (حسنا! = فارغة) {
إرجاع Integer.parseInt(ok);
} آخر {
log.warn("مشكلة"، على سبيل المثال)؛
العودة -1؛
}
});
يتم استدعاء Handle () دائمًا، وتكون النتائج والاستثناءات غير فارغة. هذه إستراتيجية شاملة.
6. اجمع بين اثنين من العقود الآجلة المكتملة معًا
يعد CompletableFuture كواحدة من العمليات غير المتزامنة أمرًا رائعًا ولكنه يُظهر حقًا مدى قوته عندما يتم دمج العديد من هذه العقود الآجلة بطرق مختلفة.
7. قم بدمج (ربط) هذين العقدين الآجلين (ثم قم بتأليف ())
في بعض الأحيان تريد التشغيل على قيمة مستقبلية معينة (عندما تكون جاهزة)، ولكن هذه الدالة تُرجع أيضًا مستقبلًا. يتسم CompletableFuture بالمرونة الكافية لفهم أنه يجب الآن استخدام نتيجة وظيفتنا كمستقبل عالي المستوى، مقارنةً بـ CompletableFuture<CompletableFuture>. الطريقة ثمCompose() تعادل flatMap الخاص بـ Scala:
انسخ رمز الكود كما يلي:
<U> CompletableFuture<U>thenCompose(Function<? super T,CompletableFuture<U>> fn);
...تتوفر أيضًا اختلافات غير متزامنة في المثال التالي، راقب بعناية الأنواع والاختلافات بين ثمApply()(map) وthenCompose()(flatMap). عند تطبيق طريقة CalculateRelevance()، يتم إرجاع CompletableFuture:
انسخ رمز الكود كما يلي:
CompletableFuture<Document> docFuture = //...
CompletableFuture<CompletableFuture<Double>> f =
docFuture.thenApply(this::calculateRelevance);
CompletableFuture<Double> relevanceFuture =
docFuture.thenCompose(this::calculateRelevance);
//...
خاص CompletableFuture<Double> احسبRelevance(Document doc) //...
تعد ثمCompose() طريقة مهمة تسمح ببناء خطوط أنابيب قوية وغير متزامنة دون حظر وانتظار الخطوات المتوسطة.
8. قيم التحويل لعقدين آجلين (ثمCombine())
عندما يتم استخدامthenCompose() لربط مستقبل يعتمد على ثمCombine آخر، عند اكتمالهما معًا، فإنه يجمع بين المستقبلين المستقلين:
انسخ رمز الكود كما يلي:
<U,V> CompletableFuture<V> ثمCombine(CompletableFuture<?extension U>other, BiFunction<?super T,?super U,?extension V> fn)
...تتوفر أيضًا متغيرات غير متزامنة، على افتراض أن لديك اثنين من CompletableFutures، أحدهما يقوم بتحميل العميل والآخر يقوم بتحميل المتجر الأخير. إنها مستقلة تمامًا عن بعضها البعض، ولكن عند اكتمالها، تريد استخدام قيمها لحساب المسار. هنا مثال محروم:
انسخ رمز الكود كما يلي:
CompletableFuture<Customer> customerFuture =loadCustomerDetails(123);
CompletableFuture<Shop> shopFuture = أقربShop();
CompletableFuture<Route> RouteFuture =
customerFuture.thenCombine(shopFuture, (cust, shop) -> findRoute(cust, shop));
//...
طريق خاص findRoute(عميل العميل، متجر المتجر) //...
يرجى ملاحظة أنه في Java 8 يمكنك ببساطة استبدال المرجع إلى أسلوب this::findRoute بـ (cust, shop) -> findRoute(cust, shop):
انسخ رمز الكود كما يلي:
customerFuture.thenCombine(shopFuture, this::findRoute);
كما تعلمون، لدينا customerFuture وshopFuture. ثم يلفهم المسار المستقبلي وينتظرهم حتى يكتملوا. عندما تكون جاهزة، سيتم تشغيل الوظيفة التي قدمناها لدمج جميع النتائج (findRoute()). سيكتمل هذا المسار المستقبلي عند اكتمال العقدين الآجلين الأساسيين واكتمال findRoute() أيضًا.
9. انتظر حتى تكتمل كافة العقود الآجلة الكاملة
إذا بدلاً من إنشاء CompletableFuture جديد يربط بين هاتين النتيجتين، نريد فقط أن يتم إعلامنا عند اكتماله، فيمكننا استخدام سلسلة الأساليبthenAcceptBoth()/runAfterBoth()، (...متغيرات Async متاحة أيضًا). تعمل بشكل مشابه لـthenAccept() وthenRun()، لكن انتظر عقدين آجلين بدلاً من واحد:
انسخ رمز الكود كما يلي:
<U> CompletableFuture<Void> ثمAcceptBoth(CompletableFuture<? Extends U>other, BiConsumer<? super T,? super U> block)
CompletableFuture<Void> runAfterBoth(CompletableFuture<?> آخر، إجراء قابل للتشغيل)
تخيل المثال أعلاه، بدلاً من إنشاء CompletableFuture جديد، فأنت تريد فقط إرسال بعض الأحداث أو تحديث واجهة المستخدم الرسومية على الفور. يمكن تحقيق ذلك بسهولة: ثمAcceptBoth():
انسخ رمز الكود كما يلي:
customerFuture.thenAcceptBoth(shopFuture, (cust, shop) -> {
طريق الطريق النهائي = findRoute(cust, shop);
// تحديث واجهة المستخدم الرسومية بالمسار
});
أتمنى أن أكون مخطئا، ولكن ربما يسأل بعض الناس أنفسهم سؤالا: لماذا لا أستطيع ببساطة منع هذين المستقبلين؟ يحب:
انسخ رمز الكود كما يلي:
Future<Customer> customerFuture =loadCustomerDetails(123);
Future<Shop> shopFuture = أقربShop();
findRoute(customerFuture.get(), shopFuture.get());
حسنًا، بالطبع يمكنك فعل ذلك. لكن النقطة الأكثر أهمية هي أن CompletableFuture يسمح بعدم التزامن، وهو نموذج برمجة يحركه الحدث بدلاً من حظر النتائج وانتظارها بفارغ الصبر. لذا من الناحية الوظيفية، فإن الجزأين المذكورين أعلاه من التعليمات البرمجية متكافئان، لكن الأخير لا يحتاج إلى شغل مؤشر ترابط للتنفيذ.
10. انتظر حتى يكمل CompletableFuture الأول المهمة
شيء آخر مثير للاهتمام هو أن CompletableFutureAPI يمكنها الانتظار حتى يكتمل المستقبل الأول (على عكس الكل). يعد هذا أمرًا مريحًا للغاية عندما يكون لديك نتائج مهمتين من نفس النوع، فأنت تهتم فقط بوقت الاستجابة، ولا تأخذ أي مهمة الأولوية. طرق واجهة برمجة التطبيقات (...تتوفر أيضًا متغيرات غير متزامنة):
انسخ رمز الكود كما يلي:
CompletableFuture <Void> AcceptEither (CompletableFuture <؟ يمتد T> آخر، كتلة المستهلك <؟ super T>)
CompletableFuture<Void> runAfterEither(CompletableFuture<?> آخر، إجراء قابل للتشغيل)
على سبيل المثال، لديك نظامين يمكن دمجهما. أحدهما لديه متوسط وقت استجابة أصغر ولكن انحراف معياري مرتفع، والآخر أبطأ بشكل عام ولكن أكثر قابلية للتنبؤ به. للحصول على أفضل ما في كلا العالمين (الأداء والقدرة على التنبؤ)، يمكنك الاتصال بكلا النظامين في نفس الوقت وانتظار أيهما ينتهي أولاً. عادةً ما يكون هذا هو النظام الأول، ولكن عندما يصبح التقدم بطيئًا، يمكن إكمال النظام الثاني في وقت مقبول:
انسخ رمز الكود كما يلي:
CompletableFuture<String> fast = fetchFast();
CompletableFuture<String> التنبؤ = fetchPredictically();
fast.acceptEither(predictable, s -> {
System.out.println("النتيجة: " + s);
});
تمثل s السلسلة التي تم الحصول عليها من fetchFast() أو fetchPredictically(). ليس علينا أن نعرف أو نهتم.
11. تحويل النظام الأول بالكامل
يعتبر ApplyToEither () هو السلف لـ AcceptEither (). عندما يكون هناك عقدان آجلان على وشك الانتهاء، يقوم الأخير ببساطة باستدعاء بعض مقتطف التعليمات البرمجية وسيقوم تطبيق ApplyToEither() بإرجاع مستقبل جديد. عندما يكتمل هذين العقدين الآجلين الأوليين، سيكتمل المستقبل الجديد أيضًا. واجهة برمجة التطبيقات (API) مشابهة إلى حد ما (...تتوفر أيضًا متغيرات غير متزامنة):
انسخ الكود كما يلي:<U> CompletableFuture<U> ApplyToEither(CompletableFuture<? Extends T>other, Function<? super T,U> fn)
يمكن إكمال وظيفة fn الإضافية هذه عند استدعاء المستقبل الأول. لست متأكدًا من الغرض من هذه الطريقة المتخصصة، ففي النهاية يمكن للمرء ببساطة استخدام: fast.applyToEither(predictable).thenApply(fn). نظرًا لأننا عالقون في واجهة برمجة التطبيقات هذه، ولكننا لا نحتاج حقًا إلى الوظائف الإضافية للتطبيق، فسأستخدم ببساطة العنصر النائب Function.identity() :
انسخ رمز الكود كما يلي:
CompletableFuture<String> fast = fetchFast();
CompletableFuture<String> التنبؤ = fetchPredictically();
CompletableFuture<String> firstDone =
fast.applyToEither(predictable, Function.<String>identity());
يمكن تشغيل أول مستقبل مكتمل. لاحظ أنه من وجهة نظر العميل، يتم إخفاء كلا العقدين الآجلين خلف firstDone. ينتظر العميل فقط اكتمال المستقبل ويستخدم applicationToEither () لإعلام العميل عند اكتمال المهمتين الأوليين.
12. المستقبل الكامل مع مجموعات متعددة
نحن نعرف الآن كيفية انتظار اكتمال عقدين آجلين (باستخدام ثمCombine()) واستكمال الأول (applyToEither()). ولكن هل يمكن أن يمتد إلى أي عدد من العقود الآجلة؟ في الواقع، استخدم أساليب المساعدة الثابتة:
انسخ رمز الكود كما يلي:
CompletableFuture ثابت<Void< allOf(CompletableFuture<?<... cfs)
static CompletableFuture<Object< AnyOf(CompletableFuture<?<... cfs)
يستخدم allOf() مجموعة من العقود الآجلة ويعيد المستقبل (في انتظار كل العوائق) عند اكتمال جميع العقود الآجلة المحتملة. من ناحية أخرى، سينتظر AnyOf() أسرع العقود الآجلة المحتملة. يرجى إلقاء نظرة على النوع العام للعقود الآجلة التي تم إرجاعها. أليس هذا ما تتوقعه؟ وسوف نركز على هذه المسألة في المقالة التالية.
تلخيص
لقد استكشفنا واجهة برمجة تطبيقات CompletableFuture بأكملها. أنا مقتنع بأن هذا لن يقهر، لذلك في المقالة التالية سننظر في تنفيذ زاحف ويب بسيط آخر باستخدام أساليب CompletableFuture وتعبيرات Java 8 lambda، وسننظر أيضًا في CompletableFuture