JavaRush /مدونة جافا /Random-AR /دليل جافا 8. 1 جزء.
ramhead
مستوى

دليل جافا 8. 1 جزء.

نشرت في المجموعة

"جافا لا تزال على قيد الحياة - وبدأ الناس في فهمها."

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

الطرق الافتراضية للواجهات

يتيح لنا Java 8 إضافة أساليب غير مجردة يتم تنفيذها في الواجهة من خلال استخدام الكلمة الأساسية الافتراضية . تُعرف هذه الميزة أيضًا بطرق الامتداد . إليك مثالنا الأول: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } بالإضافة إلى الطريقة المجردة للحساب ، تحدد واجهة الصيغة أيضًا الطريقة الافتراضية sqrt . تطبق الفئات التي تطبق واجهة الصيغة طريقة الحساب المجردة فقط . يمكن استخدام الطريقة الافتراضية sqrt مباشرة بعد إخراجها من الصندوق. Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 يتم تنفيذ كائن الصيغة ككائن مجهول. الكود مثير للإعجاب للغاية: 6 أسطر من الكود لحساب sqrt(a * 100) . كما سنرى في القسم التالي، هناك طريقة أكثر جاذبية لتنفيذ كائنات الأسلوب الفردي في Java 8.

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

لنبدأ بمثال بسيط لكيفية فرز مصفوفة من السلاسل في الإصدارات المبكرة من Java: الأسلوب المساعد الإحصائي Collections.sort يأخذ قائمة ومقارنة لفرز عناصر القائمة المحددة. ما يحدث غالبًا هو أنك تقوم بإنشاء مقارنات مجهولة وتمريرها إلى طرق الفرز. بدلاً من إنشاء كائنات مجهولة طوال الوقت، يمنحك Java 8 القدرة على استخدام بناء جملة أقل بكثير، وتعبيرات لامدا : كما ترون، فإن الكود أقصر بكثير وأسهل في القراءة. ولكن هنا يصبح الأمر أقصر: بالنسبة لطريقة السطر الواحد، يمكنك التخلص من الأقواس المتعرجة {} والكلمة الأساسية return . ولكن هنا يصبح الكود أقصر: مترجم Java على علم بأنواع المعلمات، لذا يمكنك استبعادها أيضًا. الآن دعونا نتعمق أكثر في كيفية استخدام تعبيرات لامدا في الحياة الواقعية. List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator () { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));

واجهات وظيفية

كيف تتناسب تعبيرات لامدا مع نظام نوع جافا؟ يتوافق كل لامدا مع نوع معين تحدده الواجهة. ويجب أن تحتوي الواجهة الوظيفية المزعومة على طريقة مجردة معلنة واحدة بالضبط. سيتوافق كل تعبير لامدا من نوع معين مع هذه الطريقة المجردة، وبما أن الطرق الافتراضية ليست طرقًا مجردة، فلديك الحرية في إضافة طرق افتراضية إلى واجهتك الوظيفية. يمكننا استخدام واجهة عشوائية كتعبير لامدا، بشرط أن تحتوي الواجهة على طريقة مجردة واحدة فقط. للتأكد من أن واجهتك تستوفي هذه الشروط، يجب عليك إضافة التعليق التوضيحي @FunctionalInterface . سيتم إعلام المترجم من خلال هذا التعليق التوضيحي بأن الواجهة يجب أن تحتوي على طريقة واحدة فقط، وإذا واجه طريقة مجردة ثانية في هذه الواجهة، فسوف يلقي خطأ. مثال: ضع في اعتبارك أن هذا الرمز سيكون صالحًا أيضًا حتى لو لم يتم الإعلان عن التعليق التوضيحي @FunctionalInterface . @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

مراجع للطرق والمنشئين

يمكن تبسيط المثال أعلاه بشكل أكبر باستخدام مرجع الأسلوب الإحصائي: يتيح لك Java 8 تمرير المراجع إلى الأساليب والمنشئات باستخدام :: الكلمات الأساسية الرموز . يوضح المثال أعلاه كيف يمكن استخدام الأساليب الإحصائية. ولكن يمكننا أيضًا الإشارة إلى الأساليب الخاصة بالكائنات: فلنلقي نظرة على كيفية عمل استخدام :: للمنشئين. أولاً، دعونا نحدد مثالاً بمنشئات مختلفة: بعد ذلك، نحدد واجهة مصنع PersonFactory لإنشاء كائنات شخص جديدة : Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } interface PersonFactory

{ P create(String firstName, String lastName); } بدلًا من تنفيذ المصنع يدويًا، نربط كل شيء معًا باستخدام مرجع مُنشئ: نقوم بإنشاء مرجع إلى مُنشئ فئة الشخص عبر Person::new . سيقوم مترجم Java تلقائيًا باستدعاء المنشئ المناسب عن طريق مقارنة توقيع المنشئين بتوقيع طريقة PersonFactory.create . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

منطقة لامدا

تنظيم الوصول إلى متغيرات النطاق الخارجي من تعبيرات لامدا يشبه الوصول من كائن مجهول. يمكنك الوصول إلى المتغيرات النهائية من النطاق المحلي، بالإضافة إلى حقول المثيلات والمتغيرات المجمعة.
الوصول إلى المتغيرات المحلية
يمكننا قراءة متغير محلي مع المعدل النهائي من نطاق تعبير لامدا: ولكن على عكس الكائنات المجهولة، لا يلزم إعلان المتغيرات نهائية حتى يمكن الوصول إليها من تعبير لامدا . هذا الرمز صحيح أيضًا: ومع ذلك، يجب أن يظل المتغير num غير قابل للتغيير، أي. تكون ضمنية نهائية لتجميع التعليمات البرمجية. لن يتم تجميع التعليمة البرمجية التالية: لا يُسمح أيضًا بإجراء تغييرات على num ضمن تعبير lambda. final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
الوصول إلى حقول المثيلات والمتغيرات الإحصائية
على عكس المتغيرات المحلية، يمكننا قراءة وتعديل حقول المثيلات والمتغيرات الإحصائية داخل تعبيرات لامدا. نحن نعرف هذا السلوك من الأشياء المجهولة. class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
الوصول إلى الطرق الافتراضية للواجهات
هل تتذكر المثال الذي يحتوي على مثيل الصيغة من القسم الأول؟ تحدد واجهة الصيغة طريقة sqrt الافتراضية التي يمكن الوصول إليها من كل مثيل للصيغة ، بما في ذلك الكائنات المجهولة. هذا لا يعمل مع تعبيرات لامدا. لا يمكن الوصول إلى الأساليب الافتراضية داخل تعبيرات لامدا. لا يتم تجميع التعليمات البرمجية التالية: Formula formula = (a) -> sqrt( a * 100);

واجهات وظيفية مدمجة

تحتوي JDK 1.8 API على العديد من الواجهات الوظيفية المضمنة. بعضها معروف جيدًا من الإصدارات السابقة من Java. على سبيل المثال Comparator أو Runnable . يتم توسيع هذه الواجهات لتشمل دعم لامدا باستخدام التعليق التوضيحي @FunctionalInterface . لكن Java 8 API مليئة أيضًا بالواجهات الوظيفية الجديدة التي ستجعل حياتك أسهل. بعض هذه الواجهات معروفة جيدًا من مكتبة Google Guava . حتى لو كنت على دراية بهذه المكتبة، فيجب عليك إلقاء نظرة فاحصة على كيفية توسيع هذه الواجهات، مع بعض طرق التمديد المفيدة.
المسندات
المسندات هي وظائف منطقية مع وسيطة واحدة. تحتوي الواجهة على طرق افتراضية متنوعة لإنشاء تعبيرات منطقية معقدة (و، أو، نفي) باستخدام المسندات Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
المهام
تأخذ الوظائف وسيطة واحدة وتنتج نتيجة. يمكن استخدام الطرق الافتراضية لدمج عدة وظائف معًا في سلسلة واحدة (إنشاء، ثم). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
الموردين
يقوم الموردون بإرجاع نتيجة (مثيل) من نوع أو آخر. على عكس الوظائف، لا يأخذ الموفرون الوسائط. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
المستهلكين
يمثل المستهلكون أساليب الواجهة باستخدام وسيطة واحدة. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
مقارنات
المقارنات معروفة لنا من الإصدارات السابقة من Java. يتيح لك Java 8 إضافة طرق افتراضية متنوعة إلى الواجهات. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
خيارات
واجهة الاختيارات ليست وظيفية، ولكنها أداة مساعدة رائعة لمنع NullPointerException . هذه نقطة مهمة للقسم التالي، لذلك دعونا نلقي نظرة سريعة على كيفية عمل هذه الواجهة. الواجهة الاختيارية عبارة عن حاوية بسيطة للقيم التي يمكن أن تكون فارغة أو غير فارغة. تخيل أن الطريقة يمكنها إرجاع قيمة أو لا شيء. في Java 8، بدلًا من إرجاع null ، يمكنك إرجاع مثيل اختياري . Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

تدفق

java.util.Stream عبارة عن سلسلة من العناصر يتم تنفيذ عملية واحدة أو أكثر عليها. تكون كل عملية دفق إما وسيطة أو طرفية. تُرجع العمليات الطرفية نتيجة من نوع معين، بينما تُرجع العمليات الوسيطة كائن الدفق نفسه، مما يسمح بإنشاء سلسلة من استدعاءات الأساليب. الدفق عبارة عن واجهة، مثل java.util.Collection للقوائم والمجموعات (الخرائط غير مدعومة)، ويمكن تنفيذ كل عملية دفق إما بالتسلسل أو بالتوازي. دعونا نلقي نظرة على كيفية عمل الدفق. أولاً، سنقوم بإنشاء نموذج تعليمة برمجية على شكل قائمة من السلاسل: تم تحسين المجموعات في Java 8 بحيث يمكنك إنشاء تدفقات بكل بساطة عن طريق استدعاء Collection.stream() أو Collection.parallelStream() . سيشرح القسم التالي أهم عمليات الدفق البسيطة. List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
منقي
يقبل عامل التصفية المسندات لتصفية جميع عناصر الدفق. هذه العملية متوسطة، مما يسمح لنا باستدعاء عمليات دفق أخرى (على سبيل المثال forEach) على النتيجة الناتجة (المفلترة). يقبل ForEach العملية التي سيتم تنفيذها على كل عنصر من عناصر الدفق الذي تمت تصفيته بالفعل. ForEach هي عملية طرفية. علاوة على ذلك، فإن استدعاء العمليات الأخرى أمر مستحيل. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
مرتبة
Sorted هي عملية وسيطة تُرجع تمثيلاً مرتبًا للتيار. يتم فرز العناصر بالترتيب الصحيح ما لم تحدد المقارنة الخاصة بك . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" ضع في اعتبارك أن الفرز ينشئ تمثيلاً مفروزًا للتدفق دون التأثير على المجموعة نفسها. يبقى ترتيب عناصر stringCollection دون تغيير: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
خريطة
تقوم عملية الخريطة الوسيطة بتحويل كل عنصر إلى كائن آخر باستخدام الوظيفة الناتجة. المثال التالي يحول كل سلسلة إلى سلسلة كبيرة. ولكن يمكنك أيضًا استخدام الخريطة لتحويل كل كائن إلى نوع مختلف. يعتمد نوع كائنات الدفق الناتجة على نوع الوظيفة التي تمررها إلى الخريطة. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
مباراة
يمكن استخدام عمليات المطابقة المختلفة لاختبار صحة مسند معين في علاقة الدفق. جميع عمليات المطابقة نهائية وترجع نتيجة منطقية. boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
عدد
العد عبارة عن عملية طرفية تُرجع عدد عناصر الدفق كقيمة طويلة . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
يقلل
هذه عملية طرفية تعمل على تقصير عناصر الدفق باستخدام الوظيفة التي تم تمريرها. ستكون النتيجة اختيارية تحتوي على القيمة المختصرة. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

تيارات متوازية

كما ذكر أعلاه، يمكن أن تكون التدفقات متتالية أو متوازية. يتم تنفيذ عمليات الدفق المتسلسل على خيط تسلسلي، بينما يتم تنفيذ عمليات الدفق المتوازي على عدة سلاسل متوازية. يوضح المثال التالي كيفية زيادة الأداء بسهولة باستخدام دفق متوازي. أولاً، لنقم بإنشاء قائمة كبيرة من العناصر الفريدة: الآن سنحدد الوقت المستغرق في فرز تدفق هذه المجموعة. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
تيار المسلسل
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
تيار موازي
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms كما ترون، كلا الجزأين متطابقان تقريبًا، لكن الفرز الموازي أسرع بنسبة 50٪. كل ما تحتاجه هو تغيير الدفق () إلى التوازي () .

خريطة

كما ذكرنا سابقًا، الخرائط لا تدعم التدفقات. بدلاً من ذلك، بدأت الخريطة في دعم طرق جديدة ومفيدة لحل المشكلات الشائعة. يجب أن يكون الكود أعلاه بديهيًا: يحذرنا putIfAbsent من كتابة شيكات فارغة إضافية. يقبل forEach دالة ليتم تنفيذها لكل قيمة من قيم الخريطة. يوضح هذا المثال كيفية تنفيذ العمليات على قيم الخريطة باستخدام الوظائف: بعد ذلك، سنتعلم كيفية إزالة إدخال لمفتاح معين فقط إذا كان مرتبطًا بقيمة معينة: طريقة جيدة أخرى: دمج إدخالات الخريطة أمر سهل للغاية: الدمج إما إدراج المفتاح/القيمة في الخريطة، إذا لم يكن هناك إدخال للمفتاح المحدد، أو سيتم استدعاء وظيفة الدمج، والتي ستغير قيمة الإدخال الحالي. Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null map.getOrDefault(42, "not found"); // not found map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION