لماذا تحتاج إلى الترقية إلى Java 8 بالفعل

نشرت: 2022-03-11

تم إصدار أحدث إصدار من نظام Java الأساسي ، Java 8 ، منذ أكثر من عام. لا يزال العديد من الشركات والمطورين يعملون مع الإصدارات السابقة ، وهو أمر مفهوم ، نظرًا لوجود الكثير من المشكلات في الترحيل من إصدار نظام أساسي إلى آخر. ومع ذلك ، لا يزال العديد من المطورين يبدأون تطبيقات جديدة بإصدارات قديمة من Java. هناك عدد قليل جدًا من الأسباب الوجيهة للقيام بذلك ، لأن Java 8 قد أدخلت بعض التحسينات المهمة على اللغة.

هناك العديد من الميزات الجديدة في Java 8. سأعرض لك عددًا قليلاً من الميزات المفيدة والمثيرة للاهتمام:

  • تعابير لامدا
  • دفق API للعمل مع المجموعات
  • تسلسل المهام غير المتزامن باستخدام CompletableFuture
  • العلامة التجارية الجديدة Time API

تعابير لامدا

لامدا عبارة عن كتلة تعليمات برمجية يمكن الرجوع إليها وتمريرها إلى جزء آخر من التعليمات البرمجية للتنفيذ المستقبلي مرة واحدة أو أكثر. على سبيل المثال ، الوظائف المجهولة في اللغات الأخرى هي lambdas. مثل الدوال ، يمكن تمرير Lambdas الوسيطات في وقت تنفيذها ، وتعديل نتائجها. قدم Java 8 تعبيرات lambda ، والتي تقدم بنية بسيطة لإنشاء واستخدام lambdas.

دعونا نرى مثالاً على كيف يمكن لهذا أن يحسن الكود الخاص بنا. هنا لدينا مقارن بسيط يقارن بين قيمتين صحيحين من خلال Integer 2:

 class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }

يمكن استدعاء مثيل من هذه الفئة ، في المستقبل ، في الكود حيث يلزم هذا المقارنة ، مثل هذا:

 ... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...

تتيح لنا صيغة لامدا الجديدة القيام بذلك بشكل أكثر بساطة. إليك تعبير لامدا بسيط يقوم بنفس عمل طريقة compare من BinaryComparator :

 (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

يحتوي الهيكل على العديد من أوجه التشابه مع الوظيفة. بين قوسين ، قمنا بإعداد قائمة بالحجج. يوضح النحو -> أن هذه لامدا. وفي الجزء الأيمن من هذا التعبير ، قمنا بإعداد سلوك لامدا الخاص بنا.

جافا 8 LAMBDA EXPRESSION

الآن يمكننا تحسين مثالنا السابق:

 ... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...

قد نحدد متغيرًا بهذا الكائن. دعونا نرى كيف يبدو:

 Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

الآن يمكننا إعادة استخدام هذه الوظيفة ، مثل هذا:

 ... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...

لاحظ أنه في هذه الأمثلة ، يتم تمرير لامدا إلى طريقة sort() بنفس الطريقة التي يتم بها تمرير مثيل BinaryComparator في المثال السابق. كيف يعرف JVM تفسير لامدا بشكل صحيح؟

للسماح للوظائف بأخذ lambdas كوسيطات ، يقدم Java 8 مفهومًا جديدًا: واجهة وظيفية . الواجهة الوظيفية هي واجهة تحتوي على طريقة مجردة واحدة فقط. في الواقع ، تتعامل Java 8 مع تعبيرات lambda كتطبيق خاص للواجهة الوظيفية. هذا يعني أنه من أجل الحصول على lambda كوسيطة طريقة ، فإن النوع المعلن لهذه الوسيطة يحتاج فقط إلى أن يكون واجهة وظيفية.

عندما نعلن عن واجهة وظيفية ، قد نضيف تدوين @FunctionalInterface لنُظهر للمطورين ما هي:

 @FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }

الآن ، يمكننا أن نسمي الطريقة sendDTO ، والتي تمر بأحرف لامبدا مختلفة لتحقيق سلوك مختلف ، مثل هذا:

 sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));

طريقة المراجع

تسمح لنا حجج Lambda بتعديل سلوك الوظيفة أو الطريقة. كما نرى في المثال الأخير ، أحيانًا ما تستخدم lambda فقط طريقة أخرى ( sendToAndroid أو sendToIos ). لهذه الحالة الخاصة ، يقدم Java 8 اختصارًا مناسبًا: مراجع الطريقة . يمثل بناء الجملة المختصر هذا lambda الذي يستدعي طريقة ، وله الشكل objectName::methodName . يتيح لنا ذلك جعل المثال السابق أكثر إيجازًا وقابلية للقراءة:

 sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);

في هذه الحالة ، يتم تنفيذ الأسلوبين sendToAndroid و sendToIos في this الفئة. قد نشير أيضًا إلى طرق كائن أو فئة أخرى.

دفق API

توفر Java 8 قدرات جديدة للعمل مع Collections ، في شكل Stream API جديد تمامًا. يتم توفير هذه الوظيفة الجديدة من خلال الحزمة java.util.stream ، وتهدف إلى تمكين نهج أكثر وظيفية للبرمجة مع المجموعات. كما سنرى ، هذا ممكن إلى حد كبير بفضل صيغة لامدا الجديدة التي ناقشناها للتو.

يوفر Stream API سهولة في التصفية والحساب ورسم الخرائط للمجموعات ، بالإضافة إلى طرق مختلفة للحصول على شرائح ومجموعات فرعية من المعلومات منها. بفضل بناء الجملة ذي النمط الوظيفي ، تسمح واجهة برمجة التطبيقات Stream API بتعليمات برمجية أقصر وأكثر أناقة للعمل مع المجموعات.

لنبدأ بمثال قصير. سنستخدم نموذج البيانات هذا في جميع الأمثلة:

 class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }

دعنا نتخيل أننا بحاجة إلى طباعة جميع المؤلفين في مجموعة books الذين كتبوا كتابًا بعد عام 2005. كيف يمكننا القيام بذلك في Java 7؟

 for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }

وكيف نفعل ذلك في جافا 8؟

 books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element

إنه تعبير واحد فقط! يؤدي استدعاء stream() في أي Collection إلى إرجاع كائن Stream يضم جميع عناصر تلك المجموعة. يمكن معالجة ذلك باستخدام مُعدِّلات مختلفة من Stream API ، مثل filter() و map() . يقوم كل معدل بإرجاع كائن Stream جديد مع نتائج التعديل ، والذي يمكن معالجته بشكل أكبر. تسمح لنا الطريقة .forEach() بتنفيذ بعض الإجراءات لكل مثيل من الدفق الناتج.

يوضح هذا المثال أيضًا العلاقة الوثيقة بين البرمجة الوظيفية وتعبيرات لامدا. لاحظ أن الوسيطة التي تم تمريرها إلى كل طريقة في الدفق هي إما lambda مخصصة ، أو مرجع أسلوب. من الناحية الفنية ، يمكن أن يتلقى كل معدل أي واجهة وظيفية ، كما هو موضح في القسم السابق.

تساعد Stream API المطورين على النظر إلى مجموعات Java من زاوية جديدة. تخيل الآن أننا بحاجة إلى الحصول على Map للغات المتاحة في كل بلد. كيف سيتم تنفيذ ذلك في Java 7؟

 Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }

في Java 8 ، الأشياء أكثر إتقانًا:

 import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));

طريقة collect() تسمح لنا بجمع نتائج الدفق بطرق مختلفة. هنا ، يمكننا أن نرى أنه يتم تجميعه أولاً حسب البلد ، ثم يقوم بتعيين كل مجموعة حسب اللغة. ( groupingBy() و toSet() كلاهما طريقتان ثابتتان من فئة Collectors .)

JAVA 8 STREAM API

هناك الكثير من القدرات الأخرى لـ Stream API. يمكن العثور على الوثائق الكاملة هنا. أوصي بقراءة المزيد لاكتساب فهم أعمق لجميع الأدوات القوية التي يجب أن تقدمها هذه الحزمة.

تسلسل المهام غير المتزامن باستخدام CompletableFuture

في حزمة Java 7's java.util.concurrent ، توجد واجهة Future<T> ، والتي تتيح لنا الحصول على حالة أو نتيجة لبعض المهام غير المتزامنة في المستقبل. لاستخدام هذه الوظيفة ، يجب علينا:

  1. قم بإنشاء ExecutorService ، والتي تدير تنفيذ المهام غير المتزامنة ، ويمكنها إنشاء كائنات Future لتتبع تقدمها.
  2. قم بإنشاء مهمة Runnable بشكل غير متزامن.
  3. قم بتشغيل المهمة في ExecutorService ، والتي ستوفر Future يتيح الوصول إلى الحالة أو النتائج.

من أجل الاستفادة من نتائج مهمة غير متزامنة ، من الضروري مراقبة تقدمها من الخارج ، باستخدام أساليب واجهة Future ، وعندما تكون جاهزة ، استرجع النتائج بشكل صريح وتنفيذ المزيد من الإجراءات معها. يمكن أن يكون هذا معقدًا إلى حد ما ليتم تنفيذه بدون أخطاء ، خاصة في التطبيقات التي تحتوي على عدد كبير من المهام المتزامنة.

ومع ذلك ، في Java 8 ، يتم أخذ مفهوم Future إلى أبعد من ذلك ، مع واجهة CompletableFuture<T> ، والتي تسمح بإنشاء وتنفيذ سلاسل المهام غير المتزامنة. إنها آلية قوية لإنشاء تطبيقات غير متزامنة في Java 8 ، لأنها تتيح لنا معالجة نتائج كل مهمة تلقائيًا عند الانتهاء.

دعنا نرى مثالا:

 import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);

طريقة CompletableFuture.supplyAsync تنشئ مهمة جديدة غير متزامنة في Executor الافتراضي (عادةً ForkJoinPool ). عند انتهاء المهمة ، سيتم توفير نتائجها تلقائيًا كوسيطات للدالة this::getLinks ، والتي يتم تشغيلها أيضًا في مهمة جديدة غير متزامنة. أخيرًا ، تتم طباعة نتائج هذه المرحلة الثانية تلقائيًا على System.out . thenApply() و thenAccept() هما طريقتان فقط من عدة طرق مفيدة متاحة لمساعدتك في إنشاء مهام متزامنة دون استخدام Executors يدويًا.

يُسهل برنامج CompletableFuture إدارة تسلسل العمليات المعقدة غير المتزامنة. لنفترض أننا بحاجة إلى إنشاء عملية رياضية متعددة الخطوات من ثلاث مهام. تستخدم المهمة 1 والمهمة 2 خوارزميات مختلفة للعثور على نتيجة للخطوة الأولى ، ونعلم أن أحدهما فقط سيعمل بينما سيفشل الآخر. ومع ذلك ، أي واحد يعمل يعتمد على بيانات الإدخال ، والتي لا نعرفها في وقت مبكر. يجب تلخيص نتيجة هذه المهام بنتيجة المهمة 3 . وبالتالي ، نحتاج إلى إيجاد نتيجة المهمة 1 أو المهمة 2 ، ونتيجة المهمة 3 . لتحقيق ذلك ، يمكننا كتابة شيء مثل هذا:

 import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution

إذا درسنا كيفية تعامل Java 8 مع هذا ، فسنرى أنه سيتم تشغيل جميع المهام الثلاث في نفس الوقت ، بشكل غير متزامن. على الرغم من فشل المهمة 2 مع استثناء ، سيتم حساب النتيجة النهائية وطباعتها بنجاح.

جافا 8 ASYNCHRONOUS البرمجة مع CompletableFuture

يُسهل برنامج CompletableFuture من إنشاء مهام غير متزامنة بمراحل متعددة ، ويمنحنا واجهة سهلة لتحديد الإجراءات التي يجب اتخاذها بالضبط عند الانتهاء من كل مرحلة.

جافا للتاريخ والوقت API

كما ذكر قبول Java الخاص:

قبل إصدار Java SE 8 ، تم توفير آلية التاريخ والوقت لـ Java بواسطة فئات java.util.Date و java.util.Calendar و java.util.TimeZone ، بالإضافة إلى الفئات الفرعية الخاصة بهم ، مثل java.util.GregorianCalendar . هذه الفئات لها عدة عيوب منها

  • لم تكن فئة التقويم من النوع الآمن.
  • نظرًا لأن الفئات كانت قابلة للتغيير ، فلا يمكن استخدامها في التطبيقات متعددة مؤشرات الترابط.
  • كانت الأخطاء في كود التطبيق شائعة بسبب العدد غير المعتاد للأشهر ونقص أمان النوع ".

يحل Java 8 أخيرًا هذه المشكلات طويلة الأمد ، مع حزمة java.time الجديدة ، التي تحتوي على فئات للعمل مع التاريخ والوقت. جميعها غير قابلة للتغيير ولديها واجهات برمجة تطبيقات مشابهة لإطار العمل الشهير Joda-Time ، والذي يستخدمه جميع مطوري Java تقريبًا في تطبيقاتهم بدلاً من Date الأصلي Calendar والمنطقة TimeZone .

فيما يلي بعض الفئات المفيدة في هذه الحزمة:

  • Clock - ساعة لإخبار الوقت الحالي ، بما في ذلك الوقت الحالي والتاريخ والوقت مع المنطقة الزمنية.
  • Duration Period - مقدار الوقت. يستخدم Duration قيمًا تستند إلى الوقت مثل "76.8 ثانية ، Period زمنية ، مستندة إلى التاريخ ، مثل" 4 سنوات و 6 أشهر و 12 يومًا ".
  • Instant - نقطة زمنية لحظية ، بتنسيقات متعددة.
  • LocalDate أو LocalDateTime أو LocalTime أو Year أو YearMonth - تاريخ أو وقت أو سنة أو شهر أو مزيج منها ، بدون منطقة زمنية في نظام التقويم ISO-8601.
  • OffsetDateTime ، OffsetTime - تاريخ - وقت مع إزاحة من التوقيت العالمي المنسق / غرينتش في نظام التقويم ISO-8601 ، مثل "2015-08-29T14: 15: 30 + 01: 00."
  • ZonedDateTime - تاريخ - وقت مع منطقة زمنية مرتبطة في نظام التقويم ISO-8601 ، مثل "1986-08-29T10: 15: 30 + 01: 00 أوروبا / باريس."

جافا 8 تايم API

في بعض الأحيان ، نحتاج إلى العثور على تاريخ نسبي مثل "أول ثلاثاء من الشهر". لهذه الحالات ، يوفر java.time فئة خاصة TemporalAdjuster . تحتوي فئة TemporalAdjuster على مجموعة قياسية من أدوات الضبط المتاحة كطرق ثابتة. تسمح لنا هذه بما يلي:

  • ابحث عن اليوم الأول أو الأخير من الشهر.
  • ابحث عن اليوم الأول أو الأخير من الشهر التالي أو السابق.
  • ابحث عن اليوم الأول أو الأخير من السنة.
  • ابحث عن اليوم الأول أو الأخير من العام التالي أو السابق.
  • ابحث عن أول أو آخر يوم من أيام الأسبوع خلال الشهر ، مثل "أول أربعاء في حزيران (يونيو)".
  • ابحث عن اليوم التالي أو السابق من الأسبوع ، مثل "الخميس المقبل".

إليك مثال قصير عن كيفية الحصول على أول ثلاثاء من الشهر:

 LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
هل ما زلت تستخدم Java 7؟ احصل مع البرنامج! # جافا 8
سقسقة

جافا 8 في الملخص

كما نرى ، يعد Java 8 إصدارًا تاريخيًا لمنصة Java الأساسية. هناك الكثير من التغييرات اللغوية ، لا سيما مع إدخال lambdas ، والتي تمثل خطوة لجلب المزيد من قدرات البرمجة الوظيفية إلى Java. يعد Stream API مثالًا جيدًا على كيفية قيام Lambdas بتغيير طريقة عملنا مع أدوات Java القياسية التي اعتدنا عليها بالفعل.

أيضًا ، توفر Java 8 بعض الميزات الجديدة للعمل مع البرمجة غير المتزامنة وإصلاحات ضرورية للغاية لأدوات التاريخ والوقت.

تمثل هذه التغييرات معًا خطوة كبيرة إلى الأمام بالنسبة للغة Java ، مما يجعل تطوير Java أكثر تشويقًا وفعالية.