بنيامين وينتربرج ستريم دليل ترجمة الترجمة

مرحبا يا هبر! أقدم لكم ترجمة المقال " Java 8 Stream Tutorial ".

يوفر هذا البرنامج التعليمي ، استنادًا إلى أمثلة التعليمات البرمجية ، نظرة عامة شاملة على التدفقات في Java 8. عندما قدمت لأول مرة واجهة برمجة تطبيقات Stream ، شعرت بالحيرة بسبب الاسم لأنه يتوافق مع InputStream و OutputStream من الحزمة java.io ؛ ومع ذلك ، فإن المواضيع في Java 8 هي شيء مختلف تمامًا. الخيوط هي monads التي تلعب دورًا مهمًا في تطوير البرمجة الوظيفية في Java.
في البرمجة الوظيفية ، تعد monad بنية تمثل عملية حسابية في شكل سلسلة من الخطوات المتعاقبة. يحدد نوع وهيكل الموناد سلسلة العمليات ، في حالتنا ، سلسلة من الأساليب مع وظائف مدمجة من نوع معين.
سوف يعلمك هذا البرنامج التعليمي كيفية التعامل مع التدفقات وإظهار كيفية التعامل مع الطرق المختلفة المتاحة في واجهة برمجة تطبيقات Stream. سنقوم بتحليل ترتيب العمليات ونرى كيف يؤثر تسلسل الطرق في السلسلة على الأداء. flatMap أساليب واجهة برمجة تطبيقات Stream القوية مثل reduce collect و flatMap . في نهاية الدليل ، سنولي اهتماما للعمل المتوازي مع الجداول.

إذا كنت لا تشعر بالحرية في استخدام تعبيرات lambda والواجهات الوظيفية والأساليب المرجعية ، فسيكون من المفيد لك أن تتعرف على دليلي إلى الابتكارات في Java 8 ( الترجمة في Habré) ، وبعد ذلك تعود إلى دراسة التدفقات.

كيف تعمل المواضيع


يمثل التدفق سلسلة من العناصر ويوفر طرقًا متنوعة لإجراء العمليات الحسابية على هذه العناصر:

 List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1"); myList .stream() .filter(s -> s.startsWith("c")) .map(String::toUpperCase) .sorted() .forEach(System.out::println); // C1 // C2 

طرق التدفقات متوسطة (متوسطة) ومحطة (محطة). تُرجع الطرق الوسيطة دفقًا ، والذي يسمح باستدعاء العديد من هذه الطرق بالتتابع. الأساليب الطرفية إما لا تُرجع قيمة (باطلة) أو تُرجع نتيجة لنوع آخر غير الدفق. في المثال أعلاه ، تكون filter map sorted وسيطة ، forEach منها طرفي. للحصول على قائمة كاملة بطرق التدفق المتاحة ، راجع الوثائق . تُعرف سلسلة العمليات هذه أيضًا باسم خط أنابيب التشغيل.

تقبل معظم الأساليب من Stream API كمعاملات تعبيرات lambda ، وهي واجهة وظيفية تصف السلوك المحدد لهذه الطريقة. يجب أن يكون معظمهم في وقت واحد غير متدخلين وعديمي الجنسية. ماذا يعني هذا؟

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

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

أنواع مختلفة من المواضيع


يمكن إنشاء التدفقات من البيانات المصدر المختلفة ، وخاصة من المجموعات. تدعم parllelStream() طرق stream() الجديدة stream() و parllelStream() لإنشاء تدفقات متتابعة ومتوازية. الخيوط المتوازية قادرة على العمل في وضع متعدد الخيوط (على خيوط متعددة) وستتم مناقشتها في نهاية الدليل. في غضون ذلك ، ضع في اعتبارك مؤشرات الترابط التسلسلية:

 Arrays.asList("a1", "a2", "a3") .stream() .findFirst() .ifPresent(System.out::println); // a1 

هنا ، استدعاء الأسلوب stream() في قائمة بإرجاع كائن دفق عادي.
ومع ذلك ، للعمل مع دفق ، ليس من الضروري على الإطلاق إنشاء مجموعة:

 Stream.of("a1", "a2", "a3") .findFirst() .ifPresent(System.out::println); // a1 

فقط استخدم Stream.of() لإنشاء دفق من مراجع كائن متعددة.

بالإضافة إلى تدفقات الكائنات العادية ، يحتوي Java 8 على أنواع خاصة من التدفقات للعمل مع الأنواع البدائية: int ، long ، double. كما قد تخمن ، هذا هو IntStream ، LongStream ، DoubleStream .

يمكن أن تستبدل تدفقات IntStream العادية (؛؛) IntStream.range() باستخدام IntStream.range() :

 IntStream.range(1, 4) .forEach(System.out::println); // 1 // 2 // 3 

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

  • تستخدم التدفقات البدائية تعبيرات lambda الخاصة. على سبيل المثال ، IntFunction بدلاً من Function ، أو IntPredicate بدلاً من Predicate.
  • تدفقات بدائية تدعم أساليب محطة إضافية: sum() و average()

     Arrays.stream(new int[] {1, 2, 3}) .map(n -> 2 * n + 1) .average() .ifPresent(System.out::println); // 5.0 


من المفيد في بعض الأحيان تحويل دفق الكائنات إلى دفق من العناصر البدائية أو العكس. لهذا الغرض ، تدعم تدفقات الكائن الطرق الخاصة: mapToInt() ، mapToLong() ، mapToDouble() :

 Stream.of("a1", "a2", "a3") .map(s -> s.substring(1)) .mapToInt(Integer::parseInt) .max() .ifPresent(System.out::println); // 3 

يمكن تحويل تدفقات العناصر الأولية إلى تدفقات كائنات عن طريق استدعاء mapToObj() :

 IntStream.range(1, 4) .mapToObj(i -> "a" + i) .forEach(System.out::println); // a1 // a2 // a3 

في المثال التالي ، يتم تعيين دفق من أرقام الفاصلة العائمة إلى دفق من الأعداد الصحيحة ثم يتم تعيينه إلى دفق من الكائنات:

 Stream.of(1.0, 2.0, 3.0) .mapToInt(Double::intValue) .mapToObj(i -> "a" + i) .forEach(System.out::println); // a1 // a2 // a3 

أمر التنفيذ


الآن بعد أن تعلمنا كيفية إنشاء تدفقات مختلفة وكيفية التعامل معها ، سنغطس بشكل أعمق وننظر في كيفية ظهور عمليات البث أسفل الغطاء.

سمة مهمة من الطرق الوسيطة هي كسلها . لا توجد طريقة طرفية في هذا المثال:

 Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; }); 

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

 Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; }) .forEach(s -> System.out.println("forEach: " + s)); 

تنفيذ جزء التعليمات البرمجية هذا يؤدي إلى الإخراج إلى وحدة التحكم للنتيجة التالية:

 filter: d2 forEach: d2 filter: a2 forEach: a2 filter: b1 forEach: b1 filter: b3 forEach: b3 filter: c forEach: c 

قد يفاجئ الترتيب الذي يتم به ترتيب النتائج. يمكن للمرء أن يتوقع بسذاجة أن يتم تنفيذ الأساليب "أفقيا": واحدة تلو الأخرى لجميع عناصر الدفق. ومع ذلك ، بدلاً من ذلك ، يتحرك العنصر على طول السلسلة "رأسياً". أولاً ، يمر السطر الأول من "d2" عبر طريقة filter ، ثم عبر forEach وبعد ذلك فقط ، وبعد تمرير العنصر الأول من خلال سلسلة كاملة من الأساليب ، يبدأ العنصر التالي في المعالجة.

بالنظر إلى هذا السلوك ، يمكنك تقليل العدد الفعلي للعمليات:

 Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .anyMatch(s -> { System.out.println("anyMatch: " + s); return s.startsWith("A"); }); // map: d2 // anyMatch: D2 // map: a2 // anyMatch: A2 

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

لماذا التسلسل يهم


المثال التالي يتكون من طريقتين forEach وطريقة forEach . النظر في كيفية تنفيذ هذه الأساليب:

 Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("A"); }) .forEach(s -> System.out.println("forEach: " + s)); // map: d2 // filter: D2 // map: a2 // filter: A2 // forEach: A2 // map: b1 // filter: B1 // map: b3 // filter: B3 // map: c // filter: C 

من السهل تخمين أن كل من map وطريقة filter تسمى 5 مرات أثناء وقت التنفيذ - مرة واحدة لكل عنصر من عناصر المجموعة الأصلية ، بينما يتم استدعاء forEach مرة واحدة فقط - للعنصر الذي اجتاز المرشح.

يمكنك تقليل عدد العمليات بشكل كبير عن طريق تغيير ترتيب مكالمات الطريقة عن طريق وضع filter في المقام الأول:

 Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s)); // filter: d2 // filter: a2 // map: a2 // forEach: A2 // filter: b1 // filter: b3 // filter: c 

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

نقوم بتوسيع المثال أعلاه عن طريق إضافة عملية فرز إضافية - الطريقة المصنفة:

 Stream.of("d2", "a2", "b1", "b3", "c") .sorted((s1, s2) -> { System.out.printf("sort: %s; %s\n", s1, s2); return s1.compareTo(s2); }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s)); 

الفرز هو نوع خاص من التشغيل المتوسط. هذه هي العملية التي تسمى الحالة ، لأنه لفرز مجموعة ، يجب أن تؤخذ حالتها في الاعتبار خلال العملية.

نتيجة لتنفيذ هذا الرمز ، نحصل على الإخراج التالي إلى وحدة التحكم:

 sort: a2; d2 sort: b1; a2 sort: b1; d2 sort: b1; a2 sort: b3; b1 sort: b3; d2 sort: c; b3 sort: c; d2 filter: a2 map: a2 forEach: A2 filter: b1 filter: b3 filter: c filter: d2 

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

مرة أخرى ، نقوم بتحسين تنفيذ هذا الرمز عن طريق تغيير ترتيب مكالمات الطريقة في السلسلة:

 Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .sorted((s1, s2) -> { System.out.printf("sort: %s; %s\n", s1, s2); return s1.compareTo(s2); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s)); // filter: d2 // filter: a2 // filter: b1 // filter: b3 // filter: c // map: a2 // forEach: A2 

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

إعادة استخدام الجداول


في Java 8 ، لا يمكن إعادة استخدام سلاسل الرسائل. بعد استدعاء أي طريقة محطة ، ينتهي الخيط:

 Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); stream.anyMatch(s -> true); // ok stream.noneMatch(s -> true); // exception 

استدعاء noneMatch بعد anyMatch في مؤشر ترابط واحد ينتج الاستثناء التالي:

 java.lang.IllegalStateException: stream has already been operated upon or closed at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229) at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459) at com.winterbe.java8.Streams5.test7(Streams5.java:38) at com.winterbe.java8.Streams5.main(Streams5.java:28) 

للتغلب على هذا القيد ، يجب إنشاء سلسلة رسائل جديدة لكل طريقة طرفية.

على سبيل المثال ، يمكنك إنشاء مورد لمنشئ ترابط جديد سيتم فيه تثبيت كافة الطرق الوسيطة:

 Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); streamSupplier.get().anyMatch(s -> true); // ok streamSupplier.get().noneMatch(s -> true); // ok 

كل مكالمة إلى أسلوب get تنشئ خيطًا جديدًا يمكنك من خلاله الاتصال بأمان بالطريقة النهائية المطلوبة.

طرق متقدمة


تدعم مؤشرات الترابط عددًا كبيرًا من الطرق المختلفة. لقد تعرفنا بالفعل على أهم الطرق. للتعرف على البقية ، راجع الوثائق . والآن يمكنك الغوص أكثر في أساليب أكثر تعقيدًا: flatMap ، flatMap reduce .

تشير معظم أمثلة التعليمات البرمجية في هذا القسم إلى مقتطف الشفرة التالي لإظهار العملية:

 class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name; } } List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12)); 

اجمع


Collect طريقة طرفية مفيدة للغاية ، يتم استخدامها لتحويل عناصر الدفق إلى نوع مختلف ، على سبيل المثال ، قائمة أو مجموعة أو خريطة.

يقبل Collector أداة Collect تحتوي على أربع طرق مختلفة: مورد. تراكم ، الموحد ، التشطيب. للوهلة الأولى ، يبدو هذا الأمر معقدًا للغاية ، ولكن Java 8 يدعم مختلف أدوات التجميع المدمجة من خلال فئة Collectors ، حيث يتم تطبيق الأساليب الأكثر استخدامًا.

حالة شعبية:

 List<Person> filtered = persons .stream() .filter(p -> p.name.startsWith("P")) .collect(Collectors.toList()); System.out.println(filtered); // [Peter, Pamela] 

كما ترون ، فإن إنشاء قائمة من عناصر البث أمر بسيط للغاية. لا تحتاج إلى قائمة ولكن الكثير؟ استخدم Collectors.toSet() .

في المثال التالي ، يتم تجميع الأشخاص حسب العمر:

 Map<Integer, List<Person>> personsByAge = persons .stream() .collect(Collectors.groupingBy(p -> p.age)); personsByAge .forEach((age, p) -> System.out.format("age %s: %s\n", age, p)); // age 18: [Max] // age 23: [Peter, Pamela] // age 12: [David] 

جامعي متنوعة بشكل لا يصدق. يمكنك أيضًا تجميع عناصر المجموعة ، على سبيل المثال ، تحديد متوسط ​​العمر:

 Double averageAge = persons .stream() .collect(Collectors.averagingInt(p -> p.age)); System.out.println(averageAge); // 19.0 

للحصول على إحصاءات أكثر شمولاً ، نستخدم أداة تجميع مجمعة تقوم بإرجاع كائن خاص بمعلومات: الحد الأدنى والحد الأقصى والمتوسط ​​للقيم ، ومجموع القيم وعدد العناصر:

 IntSummaryStatistics ageSummary = persons .stream() .collect(Collectors.summarizingInt(p -> p.age)); System.out.println(ageSummary); // IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23} 

المثال التالي يجمع بين جميع الأسماء في سطر واحد:

 String phrase = persons .stream() .filter(p -> p.age >= 18) .map(p -> p.name) .collect(Collectors.joining(" and ", "In Germany ", " are of legal age.")); System.out.println(phrase); // In Germany Max and Peter and Pamela are of legal age. 

يقبل جامع الاتصال فاصلًا ، وكذلك بادئة ولاحقة اختيارية.

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

 Map<Integer, String> map = persons .stream() .collect(Collectors.toMap( p -> p.age, p -> p.name, (name1, name2) -> name1 + ";" + name2)); System.out.println(map); // {18=Max, 23=Peter;Pamela, 12=David} 

لذلك ، تعرفنا على بعض أقوى المجمعين المدمجين. دعونا نحاول بناء الخاصة بك. نريد تحويل جميع عناصر الدفق إلى سطر واحد ، يتكون من أسماء كبيرة مفصولة بشريط عمودي |. للقيام بذلك ، قم بإنشاء أداة تجميع جديدة باستخدام Collector.of() . نحتاج إلى المكونات الأربعة لمجمعنا: المورد ، البطارية ، الموصل ، وحدة التشطيب.

 Collector<Person, StringJoiner, String> personNameCollector = Collector.of( () -> new StringJoiner(" | "), // supplier (j, p) -> j.add(p.name.toUpperCase()), // accumulator (j1, j2) -> j1.merge(j2), // combiner StringJoiner::toString); // finisher String names = persons .stream() .collect(personNameCollector); System.out.println(names); // MAX | PETER | PAMELA | DAVID 

نظرًا لأن السلاسل في Java غير قابلة للتغيير ، فنحن نحتاج إلى فئة مساعدة مثل StringJoiner تتيح StringJoiner إنشاء سلسلة من أجلنا. في الخطوة الأولى ، يقوم الموفر بإنشاء StringJoiner باستخدام محدد. يتم استخدام البطارية لإضافة كل اسم إلى StringJoiner .

يعرف الموصل كيفية توصيل جهازي StringJoiner بواحد. وفي النهاية ، يبني جهاز التشطيب السلسلة المطلوبة من StringJoiner s.

Flatmap


لذلك ، تعلمنا كيفية تحويل كائنات الدفق إلى أنواع أخرى من الكائنات باستخدام طريقة map . Map عبارة عن نوع من الطريقة المحدودة ، حيث يمكن تعيين كل كائن إلى كائن آخر واحد فقط. ولكن ماذا لو كنت تريد تعيين كائن واحد لكثير من الأشياء الأخرى ، أو عدم عرضه على الإطلاق؟ هذا هو المكان الذي يساعد طريقة flatMap . FlatMap يحول كل كائن دفق إلى دفق من الكائنات الأخرى. ثم يتم حزم محتويات هذه المواضيع في الدفق flatMap لطريقة flatMap .

من أجل إلقاء نظرة على flatMap ، دعونا نبني تدرجًا هرميًا مناسبًا للنوع على سبيل المثال:

 class Foo { String name; List<Bar> bars = new ArrayList<>(); Foo(String name) { this.name = name; } } class Bar { String name; Bar(String name) { this.name = name; } } 

لنقم بإنشاء بعض الكائنات:

 List<Foo> foos = new ArrayList<>(); // create foos IntStream .range(1, 4) .forEach(i -> foos.add(new Foo("Foo" + i))); // create bars foos.forEach(f -> IntStream .range(1, 4) .forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name)))); 

الآن لدينا قائمة من ثلاثة فو ، كل منها يحتوي على ثلاثة أشرطة .

يقبل FlatMap وظيفة يجب أن ترجع دفقًا من الكائنات. وبالتالي ، من أجل الوصول إلى كائنات شريط كل فو ، نحتاج فقط إلى العثور على الوظيفة المناسبة:

 foos.stream() .flatMap(f -> f.bars.stream()) .forEach(b -> System.out.println(b.name)); // Bar1 <- Foo1 // Bar2 <- Foo1 // Bar3 <- Foo1 // Bar1 <- Foo2 // Bar2 <- Foo2 // Bar3 <- Foo2 // Bar1 <- Foo3 // Bar2 <- Foo3 // Bar3 <- Foo3 

لذلك ، نجحنا في تحويل دفق من ثلاثة كائنات foo إلى دفق مكون من 9 كائنات شريطية.

أخيرًا ، يمكن تخفيض كل الشفرة أعلاه إلى خط أنابيب بسيط من العمليات:

 IntStream.range(1, 4) .mapToObj(i -> new Foo("Foo" + i)) .peek(f -> IntStream.range(1, 4) .mapToObj(i -> new Bar("Bar" + i + " <- " f.name)) .forEach(f.bars::add)) .flatMap(f -> f.bars.stream()) .forEach(b -> System.out.println(b.name)); 

FlatMap متاح أيضًا في الفئة Optional المقدمة في Java 8. تقوم FlatMap من الفصل Optional بإرجاع كائن اختياري لفئة أخرى. يمكن استخدام هذا لتجنب مجموعة من الشيكات null .

تخيل بنية هرمية مثل هذا:

 class Outer { Nested nested; } class Nested { Inner inner; } class Inner { String foo; } 

للحصول على السلسلة المتداخلة foo من كائن خارجي ، تحتاج إلى إضافة اختبارات null متعددة لتجنب NullPointException :

 Outer outer = new Outer(); if (outer != null && outer.nested != null && outer.nested.inner != null) { System.out.println(outer.nested.inner.foo); } 

يمكن تحقيق نفس الشيء باستخدام flatMap للفئة الاختيارية:

 Optional.of(new Outer()) .flatMap(o -> Optional.ofNullable(o.nested)) .flatMap(n -> Optional.ofNullable(n.inner)) .flatMap(i -> Optional.ofNullable(i.foo)) .ifPresent(System.out::println); 

تُرجع كل مكالمة إلى flatMap Optional للكائن المطلوب ، إذا كان موجودًا ، أو null إذا كان الكائن مفقودًا.

خفض


تجمع عملية التبسيط بين جميع عناصر الدفق في نتيجة واحدة. يدعم Java 8 ثلاثة أنواع مختلفة من أساليب التقليل.

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

 persons .stream() .reduce((p1, p2) -> p1.age > p2.age ? p1 : p2) .ifPresent(System.out::println); // Pamela 

تأخذ طريقة الاختزال وظيفة تجميع مع عامل ثنائي (BinaryOperator). reduce هنا هو دالة ثنائية (BiFunction) ، حيث تنتمي كلتا الوسيطتين إلى نفس النوع. في حالتنا ، لنوع الشخص . الوظيفة الثنائية هي نفسها تقريبا ، لكنها تأخذ حجة اثنين. في مثالنا ، تقارن الوظيفة عمر شخصين وتعيد عنصرًا له عمر أكبر.

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

 Person result = persons .stream() .reduce(new Person("", 0), (p1, p2) -> { p1.age += p2.age; p1.name += p2.name; return p1; }); System.out.format("name=%s; age=%s", result.name, result.age); // name=MaxPeterPamelaDavid; age=76 

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

 Integer ageSum = persons .stream() .reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2); System.out.println(ageSum); // 76 

كما ترون ، حصلنا على النتيجة 76 ، ولكن ماذا يحدث بالفعل تحت الغطاء؟

نقوم بتوسيع جزء التعليمات البرمجية أعلاه مع إخراج النص لتصحيح:

 Integer ageSum = persons .stream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s\n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2); return sum1 + sum2; }); // accumulator: sum=0; person=Max // accumulator: sum=18; person=Peter // accumulator: sum=41; person=Pamela // accumulator: sum=64; person=David 

كما ترون ، فإن وظيفة التجميع تؤدي كل العمل. يتم استدعاؤه أولاً بقيمة 0 مبدئية وأول شخص Max. في الخطوات الثلاث التالية ، يزداد المجموع باستمرار حسب عمر الشخص من الخطوة الأخيرة حتى يبلغ سن 76 عامًا.

إذن ماذا بعد؟ هل الموحد لم يسمى أبدا؟ النظر في التنفيذ الموازي لهذا الموضوع:

 Integer ageSum = persons .parallelStream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s\n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2); return sum1 + sum2; }); // accumulator: sum=0; person=Pamela // accumulator: sum=0; person=David // accumulator: sum=0; person=Max // accumulator: sum=0; person=Peter // combiner: sum1=18; sum2=23 // combiner: sum1=23; sum2=12 // combiner: sum1=41; sum2=35 

مع التنفيذ المتوازي ، نحصل على إخراج وحدة تحكم مختلفة تمامًا. الآن يتم استدعاء الموحد حقًا. , , -.

.


. ForkJoinPool ForkJoinPool.commonPool() . 5 — .

 ForkJoinPool commonPool = ForkJoinPool.commonPool(); System.out.println(commonPool.getParallelism()); // 3 

3 . JVM:

 -Djava.util.concurrent.ForkJoinPool.common.parallelism=5 

parallelStream() . parallel() .

, (thread) System.out :

 Arrays.asList("a1", "a2", "b1", "c2", "c1") .parallelStream() .filter(s -> { System.out.format("filter: %s [%s]\n", s, Thread.currentThread().getName()); return true; }) .map(s -> { System.out.format("map: %s [%s]\n", s, Thread.currentThread().getName()); return s.toUpperCase(); }) .forEach(s -> System.out.format("forEach: %s [%s]\n", s, Thread.currentThread().getName())); 

, (thread) (stream):

 filter: b1 [main] filter: a2 [ForkJoinPool.commonPool-worker-1] map: a2 [ForkJoinPool.commonPool-worker-1] filter: c2 [ForkJoinPool.commonPool-worker-3] map: c2 [ForkJoinPool.commonPool-worker-3] filter: c1 [ForkJoinPool.commonPool-worker-2] map: c1 [ForkJoinPool.commonPool-worker-2] forEach: C2 [ForkJoinPool.commonPool-worker-3] forEach: A2 [ForkJoinPool.commonPool-worker-1] map: b1 [main] forEach: B1 [main] filter: a1 [ForkJoinPool.commonPool-worker-3] map: a1 [ForkJoinPool.commonPool-worker-3] forEach: A1 [ForkJoinPool.commonPool-worker-3] forEach: C1 [ForkJoinPool.commonPool-worker-2] 

, (threads) ForkJoinPool . , (thread).

sort :

 Arrays.asList("a1", "a2", "b1", "c2", "c1") .parallelStream() .filter(s -> { System.out.format("filter: %s [%s]\n", s, Thread.currentThread().getName()); return true; }) .map(s -> { System.out.format("map: %s [%s]\n", s, Thread.currentThread().getName()); return s.toUpperCase(); }) .sorted((s1, s2) -> { System.out.format("sort: %s <> %s [%s]\n", s1, s2, Thread.currentThread().getName()); return s1.compareTo(s2); }) .forEach(s -> System.out.format("forEach: %s [%s]\n", s, Thread.currentThread().getName())); 

:

 filter: c2 [ForkJoinPool.commonPool-worker-3] filter: c1 [ForkJoinPool.commonPool-worker-2] map: c1 [ForkJoinPool.commonPool-worker-2] filter: a2 [ForkJoinPool.commonPool-worker-1] map: a2 [ForkJoinPool.commonPool-worker-1] filter: b1 [main] map: b1 [main] filter: a1 [ForkJoinPool.commonPool-worker-2] map: a1 [ForkJoinPool.commonPool-worker-2] map: c2 [ForkJoinPool.commonPool-worker-3] sort: A2 <> A1 [main] sort: B1 <> A2 [main] sort: C2 <> B1 [main] sort: C1 <> C2 [main] sort: C1 <> B1 [main] sort: C1 <> C2 [main] forEach: A1 [ForkJoinPool.commonPool-worker-1] forEach: C2 [ForkJoinPool.commonPool-worker-3] forEach: B1 [main] forEach: A2 [ForkJoinPool.commonPool-worker-2] forEach: C1 [ForkJoinPool.commonPool-worker-1] 

, sort main . sort Stream API Arrays , Java 8, — Arrays.parallelSort() . , , — :
“”, Arrays.sort.
reduce . , . , :

 List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12)); persons .parallelStream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s [%s]\n", sum, p, Thread.currentThread().getName()); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s [%s]\n", sum1, sum2, Thread.currentThread().getName()); return sum1 + sum2; }); 

, : , , :

 accumulator: sum=0; person=Pamela; [main] accumulator: sum=0; person=Max; [ForkJoinPool.commonPool-worker-3] accumulator: sum=0; person=David; [ForkJoinPool.commonPool-worker-2] accumulator: sum=0; person=Peter; [ForkJoinPool.commonPool-worker-1] combiner: sum1=18; sum2=23; [ForkJoinPool.commonPool-worker-1] combiner: sum1=23; sum2=12; [ForkJoinPool.commonPool-worker-2] combiner: sum1=41; sum2=35; [ForkJoinPool.commonPool-worker-2] 

, . , ( ), .

, ForkJoinPool , JVM. , (threads), .


Java 8 . . , , (Martin Fowler) Collection Pipelines .

JavaScript, Stream.js — JavaScript Java 8 Streams API. , Java 8 Tutorial ( ) Java 8 Nashorn Tutorial .

, , . GitHub . , .

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


All Articles