Skip to content

5. نوع Stream<T> في Java 8

5.1. مثال-01 - فئة Stream

تشبه العمليات على تدفقات Observable إلى حد كبير عمليات Streams. أحد الاختلافات هو أنه لا يمكن معالجة عنصر من Streams حتى يتم الحصول على Streams بالكامل، في حين يمكن معالجة (مراقبة) عنصر من تدفق Observable بمجرد الحصول عليه، دون انتظار الحصول على تدفق Observable بالكامل. هناك اختلاف آخر وهو أنه بمجرد الحصول على Stream، يتم استخدام قيمه عن طريق سحبها واحدة تلو الأخرى من Stream. أما بالنسبة لـ Observable، فالأمر مختلف. بمجرد أن يصدر قيمة، يتم دفع تلك القيمة إلى المشترك.

تنفذ عدة فئات مفهوم Stream. نقدم هنا فئة Stream<T>:

Image

توفر فئة Stream 39 طريقة. سنقدم بعضًا منها. انظر إلى الكود التالي:

  

package dvp.java8.streams;
 
import java.util.List;

import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple01 {
    public static void main(String[] args) {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // display 1
        personnes.stream().forEach(p -> {
            System.out.println(p);
        });
        System.out.println("----------------");
        // display 2
        personnes.stream().forEach(System.out::println);
    }
}
  • السطر 11: نقوم بإنشاء مثيل لقائمة الأشخاص؛
  • السطر 13: من هذه القائمة، نقوم بإنشاء Stream. يمكن تحويل جميع المجموعات إلى Streams بهذه الطريقة. وهذا يسمح لنا بالاستفادة من جميع أساليب هذه الفئة، مما يتيح لنا معالجة عناصر المجموعة بشكل أكثر إيجازًا مقارنةً باستخدام الحلقات. كما يسمح لنا أيضًا بالاستفادة من المعالجة المتوازية للعناصر عندما يكون ذلك ممكنًا؛
  • السطر 13: تتميز طريقة [Stream.forEach] بالتوقيع التالي:
 

نلاحظ أن معلمة الطريقة هي الواجهة الوظيفية [Consumer<T>] المعروضة في القسم 4.4 — وهي واجهة لا تحتوي سوى على طريقة واحدة تعمل على النوع T ولا تُرجع أي قيمة.

  • في الكود:

        personnes.stream().forEach(p -> {
            System.out.println(p);
});
  • [people.stream()] تنتج دفقًا من العناصر من النوع [Person] التي تغذي طريقة [forEach]. المعلمة p من النوع [Person]، وتقوم دالة لامدا المقدمة بطباعة هذه الشخصية؛

يمكن تبسيط الكود السابق على النحو التالي (السطر 18):


personnes.stream().forEach(System.out::println);

بدلاً من تمرير قيمة دالة لامدا كمعلمة، نمرر الإشارة إلى دالة موجودة، وهي في هذه الحالة دالة println لفئة System.out. وبالطبع، يجب أن يكون لهذه الدالة التوقيع الصحيح، وهو في هذه الحالة توقيع دالة [Consumer.accept]: void accept(T t). وكما ذكرنا سابقًا، ستكون معلمة دالة [accept] من النوع [Person

نحصل على النتائج التالية:

1
2
3
4
5
6
7
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}
----------------
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}

بمجرد معالجة دفق ما، لا يمكن معالجته مرة أخرى. يجب إعادة بنائه إذا كنت ترغب في معالجته مرة أخرى. يوضح ذلك الكود التالي [مثال 01ب]:


package dvp.java8.streams;
 
import java.util.stream.Stream;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple01b {
    public static void main(String[] args) {
        // people flows
        Stream<Personne> personnes = Personnes.get().stream();
        // display 1
        personnes.forEach(p -> {
            System.out.println(p);
        });
        System.out.println("----------------");
        // display 2
        personnes.forEach(System.out::println);
    }
}
  • السطر 11: لتحسين الكود، قررنا إنشاء الدفق مرة واحدة فقط. النتائج التي تم الحصول عليها هي كما يلي:

{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}
----------------
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.sourceStageSpliterator(Unknown Source)
    at java.util.stream.ReferencePipeline$Head.forEach(Unknown Source)
at dvp.java8.streams.Exemple02b.main(Exemple02b.java:18)

في كل مرة تريد استخدام تيار، يجب عليك إنشاؤه، حتى لو كان قد تم إنشاؤه مسبقًا.

5.2. مثال-02 - المعالجة المتوازية للعناصر في تيار

  

انظر إلى الكود التالي:


package dvp.java8.streams;
 
import java.util.List;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple02 {
    public static void main(String[] args) {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // display 1
        personnes.stream().forEach(Exemple02::affiche);
        System.out.println("-----------------");
        // display 2
        personnes.stream().parallel().forEach(Exemple02::affiche);
    }
 
    public static void affiche(Personne p) {
        System.out.printf("Personne %s sur thread %s%n", p, Thread.currentThread().getName());
    }
}
  • الأسطر 19–21: تطبع طريقة [display] على وحدة التحكم سلسلة JSON الخاصة بشخص ما مع اسم مؤشر الترابط التنفيذي الذي يتم فيه العرض؛
  • السطر 13: يعرض قائمة بالأشخاص. لاحظ أن معلمة طريقة [forEach] هي الإشارة إلى الطريقة الثابتة السابقة؛
  • السطر 16: نقوم بنفس الشيء، ولكن باستخدام طريقة [parallel]، نطلب معالجة عناصر الدفق بالتوازي عبر خيوط متعددة. لا يمكن إجراء كل المعالجة بالتوازي. هنا، يجب أن نفترض أن ترتيب العرض لا يهم لأن ترتيب تنفيذ الخيوط غير مضمون في المعالجة المتوازية. لاحظ أيضًا بناءً جملة سيصبح شائعًا لكل من Streams و Observables:
flux.m1(e1->...).m2(e2->..).m3(e3->...)...
  • (تابع)
    • يُنتج stream العناصر e1 التي تغذي الطريقة m1؛
    • flux.m1 هو بدوره تيار من العناصر e2 التي تغذي الطريقة m2؛
    • flux.m1.m2 هو تيار من العناصر e3 التي تغذي الطريقة m3؛

قد يتغير نوع العناصر e1 و e2 و e3 أثناء معالجة الدفق الأولي.

يؤدي تنفيذ هذا الرمز إلى النتيجة التالية:

1
2
3
4
5
6
7
Personne {"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"} sur thread main
Personne {"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"} sur thread main
Personne {"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"} sur thread main
-----------------
Personne {"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"} sur thread main
Personne {"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"} sur thread ForkJoinPool.commonPool-worker-1
Personne {"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"} sur thread ForkJoinPool.commonPool-worker-2

يمكننا أن نرى أن التنفيذ المتوازي (الأسطر 5–7) تم على ثلاثة خيوط مختلفة ولم يتبع ترتيب العناصر كما هو موضح في الأسطر 1–3. في هذا المستند، لن نركز كثيرًا على المعالجة المتوازية للعناصر في تيار (Stream)، لأن ذلك يتطلب مناقشة الشروط التي تجعل هذه المعالجة ممكنة. ثم نكتشف أن هناك عددًا قليلاً من العمليات التي يمكن تنفيذها بالتوازي. إحدى العمليات التي تصلح بشكل طبيعي للتوازي هي مجموع العناصر الرقمية في تيار، والتي سنقدمها الآن.

5.3. المثال-03 - المعالجة المتوازية لعناصر الدفق

  

انظر إلى الكود التالي (المثال 03أ):


package dvp.java8.streams;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
public class Exemple03a {
    public static void main(String[] args) {
 
        final long limite = 10_000_000L;
        // number of processors
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // list of numbers
        long début = new Date().getTime();
        List<Long> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(i);
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // sum of numbers - sequential method
        début = new Date().getTime();
        long somme = nombres.stream().reduce(0L, (s, i) -> s + i);
        System.out.printf("somme séquentielle : somme=%s, durée (ms)=%s%n", somme, new Date().getTime() - début);
    }
}
  • في السطر 22، نستخدم طريقة [reduce]، التي لها التوقيع التالي:
  • تعمل طريقة [reduce] مع عناصر من النوع T؛
  • تطبق طريقة [reduce] نفس المعالجة على جميع العناصر في دفق: يتم توفير القيمة الأولية للمجمع كمعلمة أولى. يتم توفير طريقة تنفذ الواجهة الوظيفية [BinaryOperator] [2] كمعلمة ثانية: استنادًا إلى كل عنصر والمجمع، تُرجع هذه الطريقة قيمة جديدة للمجمع. القيمة النهائية للمجمع هي القيمة التي تُرجعها طريقة [reduce]. يوضح الكود [3] هذه الآلية. طريقة [apply] هي طريقة الواجهة الوظيفية [BinaryOperator] [2]؛

لنعد إلى كود المثال:

  • السطر 12: نعرض عدد النوى التي اكتشفتها JVM؛
  • الأسطر 15-18: يتم إنشاء قائمة تضم 10 ملايين رقم؛
  • السطر 22: يتم حساب مجموع هذه الأرقام بالتسلسل باستخدام مؤشر ترابط واحد؛

نحصل على النتائج التالية:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4336 ms
somme séquentielle : somme=49999995000000, durée (ms)=225

الآن، دعونا نستبدل السطر 22 من الكود بما يلي (مثال 03ب):


long somme = nombres.stream().parallel().reduce(0L, (s, i) -> s + i);

نوجه عناصر Stream ليتم معالجتها بالتوازي باستخدام خيوط متعددة. وهذا ممكن لأن ترتيب جمع الأرقام لا يهم. وبالتالي، يمكننا تخصيص n1 رقمًا للخيط T1، و n2 رقمًا للخيط T2، ... وأخيرًا جمع النتائج التي قدمتها هذه الخيوط المختلفة. ثم نحصل على النتائج التالية:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4332 ms
somme parallèle : somme=49999995000000, durée (ms)=184

وبالتالي، لا يوجد أي تحسن في الأداء عمليًا. وسيكون هذا هو الحال غالبًا في الأمثلة التالية. إدارة الخيوط نفسها تستغرق وقتًا طويلاً. يجب أن تكون العملية التي يقوم بها كل نواة معقدة بما يكفي حتى يكون التحسن في الأداء ملحوظًا. ويوضح ذلك المثال التالي (Example03c):


package dvp.java8.streams;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.BinaryOperator;
 
public class Exemple03c {
    public static void main(String[] args) {
 
        final long limite = 10_000L;
        // number of processors
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // list of numbers
        long début = new Date().getTime();
        List<Long> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(i);
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // sum of numbers - sequential method
        début = new Date().getTime();
        BinaryOperator<Long> bo = (s, i) -> {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
            return s + i;
        };
        long somme = nombres.stream().reduce(0L, bo);
        System.out.printf("somme séquentielle : somme=%s, durée (ms)=%s%n", somme, new Date().getTime() - début);
    }
}
  • السطر 30: نستخدم طريقة [reduce] مرة أخرى، ونمرر إليها مرجع الطريقة من الأسطر 23–29 كمعلمة؛
  • السطر 28: تُرجع طريقة [bo] مجموع معلمتيها؛
  • الأسطر 24-27: بشكل مصطنع، نجعل الخيط ينتظر لمدة 1 مللي ثانية لمحاكاة العمل المكثف؛

ثم نحصل على النتائج التالية:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000 nombres en 2 ms
somme séquentielle : somme=49995000, durée (ms)=13617

الآن، إذا استبدلنا السطر 30 بما يلي:


long somme = nombres.stream().parallel().reduce(0L, bo);

نحصل على النتائج التالية:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000 nombres en 2 ms
somme séquentielle : somme=49995000, durée (ms)=1598

يمكننا أن نرى بوضوح تحسن الأداء الذي تم تحقيقه من خلال تنفيذ عملية حساب المجموع بالتوازي. لمعالجة 8 أرقام:

  • ينتظر الخيط التسلسلي 8 مرات لمدة 1 مللي ثانية، أو 8 مللي ثانية؛
  • تنتظر الخيوط المتوازية الثمانية كل منها 1 مللي ثانية في نفس الوقت (للتبسيط)، أي ما مجموعه 1 مللي ثانية للأرقام الثمانية؛

وبالتالي، يمكننا توقع أن يكون التنفيذ المتوازي أسرع بـ 8 مرات من التنفيذ التسلسلي. وهذا هو الحال تقريبًا هنا.

5.4. المثال-04 - تصفية دفق

  

انظر إلى الكود التالي:


package dvp.java8.streams;
 
import java.util.List;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple04 {
    public static void main(String[] args) {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // displays
        System.out.println("age < 28 ----------------------");
        personnes.stream().filter(p -> p.getAge() < 28).forEach(p -> {
            System.out.println(p);
        });
        System.out.println("poids < 50 ----------------------");
        personnes.stream().filter(p -> p.getPoids() < 50).forEach(p -> {
            System.out.println(p);
        });
        System.out.println("age < 28 ----------------------");
        personnes.stream().filter(p -> p.getAge() < 28).forEach(System.out::println);
        System.out.println("poids < 50 ----------------------");
        personnes.stream().filter(p -> p.getPoids() < 50).forEach(System.out::println);
    }
}
  • السطر 14: طريقة [Stream.filter] لها التوقيع التالي:
 
  • تتوقع طريقة [filter] كمعلمة مثيلًا للواجهة الوظيفية [Predicate] المعروضة في القسم 4.2، والتي الطريقة الوحيدة التي يجب تنفيذها فيها هي التالية: boolean test(T t);
  • تُرجع الطريقة [filter] عناصر الدفق التي تستوفي شرط Predicate. ولذلك تُستخدم لتصفية الدفق؛

انظر إلى الكود التالي:


package dvp.java8.streams;
 
import java.util.List;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple04 {
    public static void main(String[] args) {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // displays
        System.out.println("age < 28 ----------------------");
        personnes.stream().filter(p -> p.getAge() < 28).forEach(p -> {
            System.out.println(p);
        });
        System.out.println("poids < 50 ----------------------");
        personnes.stream().filter(p -> p.getPoids() < 50).forEach(p -> {
            System.out.println(p);
        });
        System.out.println("age < 28 ----------------------");
        personnes.stream().filter(p -> p.getAge() < 28).forEach(System.out::println);
        System.out.println("poids < 50 ----------------------");
        personnes.stream().filter(p -> p.getPoids() < 50).forEach(System.out::println);
    }
}
  • الأسطر 14-16: عرض الأشخاص الذين تقل أعمارهم عن 28 عامًا؛
  • الأسطر 18-20: عرض الأشخاص الذين يقل وزنهم عن 50؛
  • السطر 22: يقوم بنفس الشيء الذي تقوم به الأسطر 14-16 ولكن بشكل أكثر إيجازًا؛
  • السطر 24: يقوم بنفس الشيء الذي تقوم به الأسطر 18-20 ولكن بشكل أكثر إيجازًا؛

نتائج التنفيذ هي كما يلي:

age < 28 ----------------------
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
poids < 50 ----------------------
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
age < 28 ----------------------
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
poids < 50 ----------------------
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}

5.5. مثال-05 - إنشاء دفق<T2> من دفق<T1>

  

انظر إلى الشفرة التالية:


package dvp.java8.streams;

import java.util.List;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple05 {
  public static void main(String[] args) {
    // list of persons
    List<Personne> personnes = Personnes.get();
    // displays
    System.out.println("Personne --> String ----------------------");
    personnes.stream().map(p -> p.getNom()).forEach(System.out::println);
    System.out.println("Personne --> Integer ----------------------");
    personnes.stream().map(p -> p.getAge()).forEach(System.out::println);
  }
}
  • السطر 13: تحتوي طريقة [Stream.map] على التوقيع التالي:
 

المعلمة الخاصة بالطريقة [Stream.map] هي مثيل للواجهة الوظيفية [Function] المعروضة في القسم 4.3، والطريقة الوحيدة التي يجب تنفيذها هي: R apply(T t). نرى أنه، بالنظر إلى النوع T، تنتج الدالة [apply] نوعًا R. وبالتالي، ستنتج طريقة [Stream.map] تيارًا من النوع R من تيار من النوع T (يعني تيار من النوع T هنا، في عدم دقة تقنية سنحتفظ بها، تيارًا من العناصر من النوع T).

دعونا الآن نفحص الكود في المثال:

  • السطر 14: من شخص p، نحتفظ بالاسم فقط. وبذلك نحصل على تيار من Strings؛
  • السطر 14: من شخص p، نحتفظ بالاسم فقط. وبالتالي نحصل على تيار من Integer؛

النتائج التي تم الحصول عليها هي كما يلي:

1
2
3
4
5
6
7
8
Personne --> String ----------------------
jean
marie
camille
Personne --> Integer ----------------------
20
10
30

5.6. مثال-06 - طرق أخرى لفئة Stream<T>

  

نوضح بعضًا من الطرق الـ 39 لفئة Stream من خلال الشفرة التالية:


package dvp.java8.streams;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dvp.data.Personne;
import dvp.data.Personnes;
 
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.Stream;
 
public class Exemple06 {
 
    // mapper jSON
    static private ObjectMapper jsonMapper = new ObjectMapper();
 
    public static void main(String[] args) throws JsonProcessingException {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // all people
        affiche("all", personnes);
        // the 1st person
        affiche("findFirst", personnes.stream().findFirst().get());
        // any person
        affiche("findAny", personnes.stream().findAny().get());
        // people without the 1st
        affiche("skip 1", personnes.stream().skip(1L).collect(Collectors.toList()));
        // the first 2 people
        affiche("limit 2", personnes.stream().limit(2L).collect(Collectors.toList()));
        // the number of people
        affiche("count", personnes.stream().count());
        // the oldest person
        affiche("age max", personnes.stream().max(Comparator.comparingInt(Personne::getAge)).get());
        // the lightest person
        affiche("poids min", personnes.stream().min(Comparator.comparingDouble(Personne::getPoids)).get());
        // last person in alphabetical order of name
        affiche("nom max", personnes.stream().max((p1, p2) -> p1.getNom().compareToIgnoreCase(p2.getNom())).get());
        // total age of all persons
        affiche("âge total (reduce)", personnes.stream().map(p -> p.getAge()).reduce(0, (a1, a2) -> a1 + a2));
        // people by ascending age
        affiche("personnes par âge croissant",
                personnes.stream().sorted(Comparator.comparingInt(Personne::getAge)).collect(Collectors.toList()));
        // are there any people over 100?
        affiche("des personnes de + de 100 ans (anyMatch)", personnes.stream().anyMatch(p -> p.getAge() > 100));
        // are all people at most 100 years old?
        affiche("des personnes de + de 100 ans (noneMatch)", personnes.stream().noneMatch(p -> p.getAge() > 100));
        // are all people over 8 years old?
        affiche("des personnes de + de 8 ans (allMatch)", personnes.stream().allMatch(p -> p.getAge() > 8));
        // group people by gender
        affiche("personnes regroupées par sexe", personnes.stream().collect(Collectors.groupingBy(p -> p.getSexe())));
        // remove duplicate elements from a list
        affiche("distinct", Stream.of(1, 2, 1).distinct().collect(Collectors.toList()));
        // of a Stream<Stream<T>>, we make a Stream<T>
        affiche("flatMap", Stream.of(1, 2, 3).flatMap(i -> Stream.of(i, i + 10)).collect(Collectors.toList()));
        // of a Stream<Stream<Integer>>, we make a IntStream and calculate its sum
        affiche("flatMapToInt", Stream.of(1, 2, 3).flatMapToInt(i -> IntStream.of(i, i + 10)).sum());
        // of a Stream<Stream<Integer>>, we make a DoubleStream and then an array
        affiche("flatMapToDouble", Stream.of(1, 2, 3).flatMapToDouble(i -> DoubleStream.of(i, i * 1.2)).toArray());
        // max of a stream of integers
        affiche("reduce Integer::max", Stream.of(1, 10, 8).reduce(Integer::max).get());
        // min of a Double
        affiche("reduce Integer::min", Stream.of(1.5, 10.4, 8.9).reduce(Double::min).get());
        // average of a stream of integers
        affiche("IntStream average", IntStream.of(1, 10, 8).average().getAsDouble());
        // statistics for a stream of integers
        affiche("IntStream summaryStatistics", IntStream.of(1, 10, 8).summaryStatistics());
    }
 
    public static <T> void affiche(String message, T value) throws JsonProcessingException {
        System.out.println(String.format("%s ----", message));
        System.out.println(jsonMapper.writeValueAsString(value));
    }
}
  • السطران 72 و75: يعرضان سلسلة JSON للمعلمة الثانية للطريقة؛
  • السطر 24: يعرض سلسلة JSON لجميع الأشخاص. والنتيجة هي كما يلي:
all ----
[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"},{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}]

5.6.1. [findFirst]


// la 1ère personne
affiche("findFirst", personnes.stream().findFirst().get());

تُرجع الطريقة [findFirst] العنصر الأول من الدفق، إن وجد. وتكون صيغتها كما يلي:

النتيجة من النوع Optional<T>، وهو نوع تم تقديمه في Java 8:

تسمح فئة Optional<T> بمعالجة مختلفة للمؤشرات التي قيمتها null. يمكن لأي دالة تحتاج إلى إرجاع نوع T قد تكون قيمته null أن تختار إرجاع نوع Optional<T>. تسمح لك الدالة [Optional<T>.isPresent()] بتحديد ما إذا كانت الدالة قد أرجعت قيمة أم لا. يوضح الكود التالي [Example06b] جزءًا من كيفية عمل Optional<T>:


package dvp.java8.streams;
 
import java.util.Optional;
 
import com.fasterxml.jackson.core.JsonProcessingException;
 
public class Exemple06b {
 
    public static void main(String[] args) throws JsonProcessingException {
        // optional without value
        Optional<Integer> o1 = m1();
        System.out.println(o1.isPresent());
        affiche(o1);
        // optional with value
        Optional<Integer> o2 = m2();
        System.out.println(o2.isPresent());
        affiche(o2);
    }
 
    private static void affiche(Optional<Integer> o1) {
        try {
            // retrieve the value of the Optional
            // throws 1 exception if no value
            System.out.println(o1.get());
        } catch (Throwable th) {
            System.out.printf("%s : %s%n", th.getClass().getName(), th.getMessage());
        }
 
    }
 
    public static Optional<Integer> m1() {
        // no value
        return Optional.empty();
    }
 
    public static Optional<Integer> m2() {
        // a value
        return Optional.of(10);
    }
}

النتائج هي كما يلي:


false
java.util.NoSuchElementException : No value present
true
10

لنعد إلى الكود الذي يوضح طريقة [findFirst]:


// la 1ère personne
affiche("findFirst", personnes.stream().findFirst().get());
  • السطر 2: لتبسيط الكود، نستخدم طريقة [get] على Optional<Person> الناتج عن طريقة [findFirst]. يتطلب الكود النظيف استدعاء طريقة [Optional<Person>.isPresent()] قبل استدعاء طريقة [get]؛

والنتيجة هي كما يلي:

findFirst ----
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}

5.6.2. [findAny]


// n'importe quelle personne
affiche("findAny", personnes.stream().findAny().get());

تتميز طريقة [findAny] بالتوقيع التالي:

 

يمكن أن تُرجع الطريقة [findAny] أي عنصر من الدفق. أثناء الاختبار، لاحظنا أن التنفيذ التسلسلي يُرجع العنصر الأول من الدفق، في حين أن التنفيذ المتوازي يمكنه بالفعل إرجاع أي عنصر. ويتضح ذلك من خلال الكود التالي [Example06c]:


package dvp.java8.streams;
 
import java.util.List;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple06c {
 
    // mapper jSON
    static private ObjectMapper jsonMapper = new ObjectMapper();
 
    public static void main(String[] args) throws JsonProcessingException {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // everyone
        affiche("all", personnes);
        // any person
        affiche("findAny parallèle", personnes.stream().parallel().findAny().get());
        // any person
        affiche("findAny séquentiel", personnes.stream().findAny().get());
    }

    public static <T> void affiche(String message, T value) throws JsonProcessingException {
        System.out.println(String.format("%s ----", message));
        System.out.println(jsonMapper.writeValueAsString(value));
    }
}
  • السطر 22: تم تنفيذ findAny بالتوازي؛
  • السطر 24: تم تنفيذ findAny بالتسلسل؛

النتائج التي تم الحصول عليها هي كما يلي:

1
2
3
4
5
6
all ----
[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"},{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}]
findAny parallèle ----
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}
findAny séquentiel ----
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
  • السطر 4: أعاد التنفيذ المتوازي العنصر الثاني من قائمة الأشخاص. كان من الممكن أن يكون عنصرًا آخر؛
  • السطر 6: أرجع التنفيذ التسلسلي العنصر الأول من قائمة الأشخاص؛

يبدو أن استخدام طريقة [findAny] لا يكون منطقيًا إلا في المعالجة المتوازية لتيار.

5.6.3. [تخطي]


// les personnes sans la 1ère
affiche("skip 1", personnes.stream().skip(1L).collect(Collectors.toList()));

تحتوي طريقة [skip] على التوقيع التالي:

 

تقوم طريقة [skip] بتخطي العناصر n الأولى من دفق. كما هو موضح في الوثائق أعلاه، فإن تنفيذ هذه الطريقة بالتوازي لا يحقق سوى مكاسب ضئيلة في الأداء، بل وقد يؤدي إلى انخفاض في الأداء. في الواقع، لتخطي العناصر n الأولى، تضطر الخيوط إلى التنسيق، مما يلغي مكاسب الأداء الناتجة عن التوازي.

تُرجع الطريقة [skip] كائن Stream<Person> يتم تحويله إلى List<Person> بواسطة الطريقة [collect]، التي لها التوقيع التالي:

 

تأخذ طريقة [collect] كمعلمة مثيلًا من النوع [Collector]، الذي يتميز بتوقيع معقد. توجد تطبيقات محددة مسبقًا للنوع [Collector] تسمح لك عادةً بتجنب تنفيذها بنفسك. هنا، يتم استخدام تطبيق [Collectors.toList()]. [Collectors] هي فئة تحتوي على العديد من الطرق الثابتة التي تنفذ النوع [Collector<T,A,R>]. هذا هو المكان الأول الذي يجب البحث فيه عندما تريد تحويل دفق (Stream) إلى مجموعة Java قياسية:

 

سنستخدم بعض هذه الطرق لاحقًا.

يؤدي التنفيذ إلى النتيجة التالية:

skip 1 ----
[{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"},{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}]

تم حذف العنصر الأول من القائمة (jean).

5.6.4. [limit]


// les 2 premières personnes
affiche("limit 2", personnes.stream().limit(2L).collect(Collectors.toList()));

تحتوي طريقة [limit] على التوقيع التالي:

 

تسمح لك طريقة [limit] بالاحتفاظ فقط بأول n عنصر من دفق. وهي غير مناسبة للمعالجة المتوازية.

يؤدي التنفيذ إلى النتيجة التالية:

limit 2 ----
[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]

5.6.5. [count]


// le nombre de personnes
affiche("count", personnes.stream().count());

تتميز طريقة [count] بالتوقيع التالي:

 

تُرجع الطريقة [count] عدد العناصر الموجودة في دفق. لا يؤدي التنفيذ المتوازي لهذه الطريقة إلى تحسن في الأداء، كما هو موضح في الكود التالي (Example06d1):


package dvp.java8.streams;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Stream;
 
public class Exemple06d1 {
    public static void main(String[] args) {
 
        final long limite = 10_000_000L;
        // number of processors
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // list of numbers
        long début = new Date().getTime();
        List<Long> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(i);
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // counting numbers - sequential method
        Stream<Long> sNombres = nombres.stream();
        début = new Date().getTime();
        long count = sNombres.count();
        System.out.printf("comptage séquentiel : compteur=%s, durée (ms)=%s%n", count, new Date().getTime() - début);
    }
}
  • الأسطر 11–22: إنشاء دفق من 10 ملايين رقم؛
  • الأسطر 22–24: عد الدفق؛

ينتج عن التنفيذ النتيجة التالية:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4407 ms
comptage séquentiel : compteur=10000000, durée (ms)=67

إذا استبدلنا السطر 22 من الكود بما يلي (مثال 06d2):


Stream<Long> sNombres = nombres.stream().parallel();

نحصل على النتائج التالية:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4341 ms
comptage parallèle : compteur=10000000, durée (ms)=100

5.6.6. [الحد الأقصى، الحد الأدنى]


// la personne la + âgée
affiche("age max", personnes.stream().max(Comparator.comparingInt(Personne::getAge)).get());

تحتوي طريقة [max] على التوقيع التالي:

 

تُرجع الطريقة [max] القيمة القصوى لتيار البيانات باستخدام المقارن الذي تم تمريره إليها كمعلمة. Comparator هي واجهة وظيفية تحتوي على طريقة واحدة للتنفيذ لها التوقيع التالي: int compare(T o1, T o2). يجب أن تُرجع هذه الطريقة -1 إذا كان o1 < o2، و0 إذا كان o1.equals(o2)، و+1 إذا كان o1 > o2. تحتوي الواجهة الوظيفية Comparator على العديد من الطرق الثابتة الافتراضية التي تنفذ واجهة Comparator للحالات الأكثر شيوعًا. وبالتالي، في العبارة:


affiche("age max", personnes.stream().max(Comparator.comparingInt(Personne::getAge)).get());

نستخدم الطريقة الثابتة [Comparator.comparingInt]، التي يكون توقيعها كما يلي:

 

نوع ToIntFunction هو واجهة وظيفية:

 

تُنتج طريقة [applyAsInt] الخاصة بالواجهة الوظيفية ToIntFunction نوع int من نوع T. لنعد إلى كودنا:


affiche("age max", personnes.stream().max(Comparator.comparingInt(Personne::getAge)).get());

يجب أن يكون المعامل الفعلي لطريقة [Comparator.comparingInt] هو Person --> int lambda هنا. نمرر المرجع إلى طريقة [Person.getAge]، التي تحمل هذا التوقيع. في النهاية، سنحصل على الشخص الأكبر سنًا. نحصل على نوع Optional<Person>، ونستخرج القيمة منه باستخدام طريقة [Optional.get]. نحصل على النتيجة التالية:

age max ----
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}

لا يؤدي حساب الحد الأقصى بالتوازي إلى أي مكاسب في الأداء، كما هو موضح في المثال التالي: (مثال 06e1):


package dvp.java8.streams;
 
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.stream.Stream;
 
public class Exemple06e1 {
    public static void main(String[] args) {
 
        // data
        // final long limit = 100L;
        // final boolean verbose = true;
        final long limite = 10_000_000L;
        final boolean verbose = false;
 
        // number of processors
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // list of numbers
        long début = new Date().getTime();
        List<Long> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(new Random().nextLong());
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // max numbers - sequential method
        Stream<Long> sNombres = nombres.stream();
        Comparator<Long> compLong = (l1, l2) -> {
            if (verbose) {
                // thread
                System.out.printf("[%s]", Thread.currentThread().getName());
            }
            // comparison
            long v1 = l1.longValue();
            long v2 = l2.longValue();
            if (v1 < v2) {
                return -1;
            } else {
                if (v1 == v2) {
                    return 0;
                } else {
                    return +1;
                }
            }
        };
        début = new Date().getTime();
        // long max = sNombres.max(Comparator.naturalOrder()).get();
        long max = sNombres.max(compLong).get();
        System.out.printf("%nmax séquentiel : max=%s, durée (ms)=%s%n", max, new Date().getTime() - début);
    }
}
  • السطر 29: لدينا دفق من الأعداد الصحيحة العشوائية من النوع Long؛
  • الأسطر 30–47: المتغير lambda compLong ينفذ واجهة Comparator<Long>. عادةً ما يتم تنفيذ هذه الواجهة بواسطة الطريقة [Comparator.naturalOrder()] في السطر 49. لكن هنا، نريد عرض مؤشر ترابط التنفيذ (الأسطر 31–33). لذا نقوم بتنفيذ الواجهة بأنفسنا؛
  • السطر 50: إيجاد القيمة القصوى؛

نحصل على النتائج التالية:

 

الآن، إذا استبدلنا السطر 27 بما يلي (مثال 06e2):


Stream<Long> sNombres = nombres.stream().parallel();

نحصل على النتائج التالية:

 

وبالتالي، كان التنفيذ المتوازي أبطأ. إذا قمنا بزيادة عدد الأرقام إلى 10 ملايين باستخدام verbose=false، نحصل على النتائج التالية:

1
2
3
4
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 3764 ms

max séquentiel : max=9223370471463514417, durée (ms)=53

للتنفيذ التسلسلي:

1
2
3
4
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 3760 ms

max parallèle : max=9223365260999360873, durée (ms)=77

للتنفيذ المتوازي، الذي يظل أبطأ.

نستخدم طريقة [Stream.min] بطريقة مماثلة:


// la personne la + légère
affiche("poids min", personnes.stream().min(Comparator.comparingDouble(Personne::getPoids)).get());

5.6.7. [reduce]


// l'âge total de toutes les personnes
affiche("âge total (reduce)", personnes.stream().map(p -> p.getAge()).reduce(0, (a1, a2) -> a1 + a2));

تم تقديم طريقة [reduce] في القسم 5.3. السطر 2 أعلاه يحسب مجموع أعمار جميع الأشخاص. والنتيجة هي كما يلي:

âge total (reduce) ----
60

5.6.8. [sorted]


// les personnes par âge croissant
affiche("personnes par âge croissant",
                personnes.stream().sorted(Comparator.comparingInt(Personne::getAge)).collect(Collectors.toList()));
// les personnes par ordre alphabétique des noms
List<Personne> lPersonnes=personnes.stream().sorted((p1, p2) -> p1.getNom().compareTo(p2.getNom())).collect(Collectors.toList());
affiche("personnes par ordre alphabétique des noms", lPersonnes);

تحتوي الطريقة [sorted] (السطران 3 و 5) على التوقيع التالي:

 

تأخذ الطريقة [sorted] كمعلمة نوع [Comparator] الموصوف في القسم 5.6.6 فيما يتعلق بالطريقتين min و max. وهي تسمح بفرز دفق (Stream) وفقًا لترتيب أداة المقارنة التي يتم تمريرها إليها كمعلمة. لقد رأينا أن واجهة [Comparator] توفر عدة طرق ثابتة افتراضية تنفذ مقارنات شائعة، لا سيما للأرقام والسلاسل. هنا، نستخدم طريقة [Comparator.comparingInt]، التي تأخذ كمعلمة نوع ToIntFunction، وهو واجهة وظيفية لطريقة [applyAsInt] بالتوقيع التالي: int applyAsInt(T t). هنا، المعلمة الفعلية التي تم تمريرها إلى طريقة [Comparator.comparingInt] في السطر 3 هي الإشارة إلى طريقة [Person.age]، التي تُرجع عمر الشخص.

لا توفر واجهة [Comparator] طرقًا ثابتة لمقارنة السلاسل. في السطر 5، نقوم بأنفسنا بإنشاء لامدا تنفذ الطريقة الوحيدة لهذه الواجهة: int compare(T t1, T t2)


(p1, p2) -> p1.getNom().compareTo(p2.getNom())

يقارن هذا اللامدا أسماء الأشخاص. النتائج التي تم الحصول عليها هي كما يلي:

1
2
3
4
personnes par âge croissant ----
[{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"},{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}]
personnes par ordre alphabétique des noms ----
[{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"},{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]

لا يبدو أن التنفيذ المتوازي لعملية الفرز ممكن، كما هو موضح في الكود التالي (مثال 06f1):


package dvp.java8.streams;
 
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Exemple06f1 {
    // mapper jSON
    static ObjectMapper jsonMapper = new ObjectMapper();
 
    public static void main(String[] args) throws JsonProcessingException {
 
        // data
        final long limite = 100L;
        final boolean verbose = true;
//         final long limit = 10_000_000L;
//         final boolean verbose = false;
 
        // number of processors
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // list of numbers
        long début = new Date().getTime();
        List<Integer> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(new Random().nextInt(1000));
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // number sorting - sequential method
        Stream<Integer> sNombres = nombres.stream();
        début = new Date().getTime();
        Comparator<Integer> compInt = (i1, i2) -> {
            if (verbose) {
                // thread
                System.out.printf("[%s]", Thread.currentThread().getName());
            }
            // comparison
            int v1 = i1.intValue();
            int v2 = i2.intValue();
            if (v1 < v2) {
                return +1;
            } else {
                if (v1 == v2) {
                    return 0;
                } else {
                    return -1;
                }
            }
        };
        if (verbose) {
            affiche("nombres", sNombres.sorted(compInt).collect(Collectors.toList()));
        }
        System.out.printf("tri séquentiel : durée (ms)=%s%n", new Date().getTime() - début);
    }
 
    public static <T> void affiche(String message, T value) throws JsonProcessingException {
        System.out.println(String.format("%s ----", message));
        System.out.println(jsonMapper.writeValueAsString(value));
    }
 
}
  • الأسطر 30–36: نقوم بإنشاء تيار من الأرقام العشوائية؛
  • السطر 32: نمرر دالة لامدا compInt (الأسطر 38-55) إلى الدالة [sorted]. تقوم دالة لامدا هذه بفرز الأرقام بترتيب تنازلي وتعرض الخيط الذي يقوم بتنفيذها.

النتائج التي تم الحصول عليها هي كما يلي:

 

إذا استبدلنا، في الكود السابق، السطر 36 بما يلي (مثال 06f2):


        Stream<Integer> sNombres = nombres.stream().parallel();        

نحصل على النتائج التالية:

 

والمثير للدهشة أننا نجد أن تدفق الأرقام تم فرزه باستخدام مؤشر ترابط واحد. لم يكن هناك أي توازي. أم أنني أغفل شيئًا ما؟

5.6.9. [anyMatch, noneMatch, allMatch]


// y-a-t-il des personnes de + de 100 ans ?
affiche("des personnes de + de 100 ans (anyMatch)", personnes.stream().anyMatch(p -> p.getAge() > 100));
// est-ce que toutes les personnes ont au plus 100 ans ?
affiche("des personnes de + de 100 ans (noneMatch)", personnes.stream().noneMatch(p -> p.getAge() > 100));
// est-ce que toutes les personnes ont + de 8 ans
affiche("des personnes de + de 8 ans (allMatch)", personnes.stream().allMatch(p -> p.getAge() > 8));

الأسطر 2 و4 و6: تأخذ الطرق [anyMatch وnoneMatch وallMatch] نوع Predicate كمعلمة، كما هو موضح في القسم 4.2. وبالتالي، فإنها تقوم بالتصفية. وتُرجع الطرق الثلاث قيمة منطقية:

  • تُرجع anyMatch القيمة true إذا كان هناك عنصر واحد على الأقل في الدفق يفي بمعيار التصفية؛
  • تُرجع noneMatch القيمة true إذا لم تكن هناك عناصر في الدفق تستوفي شروط التصفية؛
  • تُرجع allMatch القيمة true إذا كانت جميع عناصر الدفق تستوفي شروط التصفية؛

النتائج التي تم الحصول عليها هي كما يلي:

1
2
3
4
5
6
des personnes de + de 100 ans (anyMatch) ----
false
des personnes de + de 100 ans (noneMatch) ----
true
des personnes de + de 8 ans (allMatch) ----
true

5.6.10. [collect(Collectors.groupingBy)]


// on regroupe les personnes par sexe
affiche("personnes regroupées par sexe", personnes.stream().collect(Collectors.groupingBy(p -> p.getSexe())));

تم تقديم الطريقة [collect] في القسم 5.6.3. معلمتها هي تنفيذ لواجهة [Collector]. توفر فئة [Collectors] عددًا من الطرق الثابتة التي تنفذ واجهة [Collector]. حتى الآن، استخدمنا طريقة [Collectors.toList()]. هنا، نستخدم الطريقة الثابتة [Collectors.groupingBy]، التي تنشئ قاموسًا من الدفق. وتكون صيغتها كما يلي:

 

تنشئ طريقة [groupingBy] Map<K,List<T>> من Stream<T>. يتم توفير المفتاح K بواسطة معلمة طريقة [groupingBy] من النوع Function<T,K>، والتي تحتوي طريقتها الوحيدة على التوقيع: K apply(T t). إذا أردنا إنشاء خريطة مفهرسة حسب جنس الشخص، يجب أن نوفر دالة تولد الجنس من الشخص. هنا، نمرر الإشارة إلى طريقة [Person.getGender] كمعلمة فعلية لطريقة [groupingBy]. النتائج التي تم الحصول عليها هي كما يلي:

personnes regroupées par sexe ----
{"HOMME":[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}],"FEMME":[{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"},{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}]}

يحتوي السطر 2 على سلسلة JSON لقاموس مفهرس بواسطة مفتاحين: MAN و WOMAN.

لا يؤدي الحساب المتوازي إلى تحسين الأداء، كما هو موضح في المثال التالي (Example06g1):


package dvp.java8.streams;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Exemple06g1 {
 
    // mppeur jSON
    static ObjectMapper jsonMapper = new ObjectMapper();
 
    public static void main(String[] args) throws JsonProcessingException {
 
        // data
        final long limite = 100L;
        final boolean verbose = true;
//         final long limit = 10_000_000L;
//         final boolean verbose = false;
 
        // number of processors
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // list of numbers
        long début = new Date().getTime();
        List<Integer> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(new Random().nextInt(1000));
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // grouping numbers by hundred - sequential method
        Stream<Integer> sNombres = nombres.stream();
        Function<Integer, Integer> groupByCent = n -> {
            if (verbose) {
                System.out.printf("[%s]", Thread.currentThread().getName());
            }
            return n / 100;
        };
        début = new Date().getTime();
        // Map<Integer, List<Integer>> lNombres = sNombres.collect(Collectors.groupingBy(number -> number / 100));
        Map<Integer, List<Integer>> lNombres = sNombres.collect(Collectors.groupingBy(groupByCent));
        System.out.printf("%nregroupement séquentiel : durée (ms)=%s%n", new Date().getTime() - début);
        // results
        if (verbose) {
            affiche("nombres regroupés", lNombres);
        }
    }
 
    public static <T> void affiche(String message, T value) throws JsonProcessingException {
        System.out.println(String.format("%s ----", message));
        System.out.println(jsonMapper.writeValueAsString(value));
    }
 
}
  • الأسطر 23–38: إنشاء تيار من الأرقام؛
  • السطر 47: يتم تجميع الأرقام في مجموعات من مئات. تُستخدم دالة لامدا في الأسطر 39–44 لعرض مؤشر ترابط التنفيذ؛

نتائج التنفيذ هي كما يلي:

 

إذا استبدلنا، في الكود، السطر 38 بالسطر التالي (مثال 06g2):


Stream<Integer> sNombres = nombres.stream().parallel();            

نحصل على النتائج التالية:

 

يمكننا أن نلاحظ أن التنفيذ المتوازي لعملية التجميع قد أدى إلى انخفاض الأداء.

5.6.11. [distinct]


// supression des éléments en double d'une liste
affiche("distinct", Stream.of(1, 2, 1).distinct().collect(Collectors.toList()));

تتميز طريقة [distinct] بالتوقيع التالي:

 

تقوم بإزالة التكرارات من دفق. طريقة [Stream.of] (السطر 2) لها التوقيع التالي:

 

تسمح لك بإنشاء دفق من قيم مقدمة بشكل صريح. نتائج التنفيذ هي كما يلي:

distinct ----
[1,2]

5.6.12. [flatMap]


// d'un Stream<Stream<T>>, on fait un Stream<T>
affiche("flatMap", Stream.of(1, 2, 3).flatMap(i -> Stream.of(i, i + 10)).collect(Collectors.toList()));

تتميز طريقة [flatMap] بالتوقيع التالي:

 

تأخذ طريقة [flatMap] كمعلمة دالة:

  • تأخذ عنصرًا من النوع T من الدفق كمعلمة؛
  • تُرجع Stream<R>؛

إذا استخدمنا الطريقة [map] الموضحة في القسم 5.5 بدلاً من الطريقة [flatMap]، فسيكون الناتج من النوع Stream<Stream<R>>، حيث كان كل عنصر من النوع T في الدفق الأولي سيُنتج عنصر Stream<R>. من ناحية أخرى، تُرجع الطريقة [flatMap] نوع Stream<R>. فهي تُسطح مختلف تدفقات Stream<R> إلى تدفق واحد. وهذا ما تظهره نتائج تنفيذ الكود السابق:

flatMap ----
[1,11,2,12,3,13]

هناك متغيرات متخصصة لـ [flatMap]:


// d'un Stream<IntStream>, on fait un IntStream dont on calcule la somme
affiche("flatMapToInt", Stream.of(1, 2, 3).flatMapToInt(i -> IntStream.of(i, i + 10)).sum());

تحتوي طريقة [flatMapToInt] على التوقيع التالي:

 

تأخذ الطريقة [flatMapToInt] كمعلمة دالة تُرجع IntStream من النوع التالي:

 

IntStream هو تيار من int. يُفضل هذا النوع على نوع Stream<Integer> لأن معالجته تتجنب عملية التعبئة/التفريغ بين أنواع Integer و int. ترث هذه الواجهة العديد من الطرق من نوع Stream<T> وتضيف طرقًا أخرى، بما في ذلك الطريقة [sum] أعلاه، التي تجمع عناصر IntStream.

يوضح الكود التالي استخدام الطريقة المماثلة [flatMapToDouble]:


// d'un Stream<DoubleStream>, on fait un DoubleStream puis un tableau
affiche("flatMapToDouble", Stream.of(1, 2, 3).flatMapToDouble(i -> DoubleStream.of(i, i * 1.2)).toArray());

تسمح لك طريقة [DoubleStream.toArray] بالتحويل من نوع DoubleStream إلى نوع double[].

فيما يلي نتائج هذين المثالين:

1
2
3
4
flatMapToInt ----
42
flatMapToDouble ----
[1.0,1.2,2.0,2.4,3.0,3.5999999999999996]

يوضح المثال التالي مكاسب الأداء التي تحققت من خلال التبديل من نوع Stream<Long> إلى نوع LongStream (مثال 06i1):


package dvp.java8.streams;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
public class Exemple06i1 {
    public static void main(String[] args) {
 
        final long limite = 10_000_000L;
        // number of processors
        System.out.printf("La JVM a détecté [%s] coeurs sur votre machine%n", Runtime.getRuntime().availableProcessors());
        // list of numbers
        long début = new Date().getTime();
        List<Long> nombres = new ArrayList<>();
        for (long i = 0; i < limite; i++) {
            nombres.add(i);
        }
        System.out.printf("création de la liste des %s nombres en %s ms%n", limite, new Date().getTime() - début);
        // sum of numbers - sequential method
        début = new Date().getTime();
        long somme = nombres.stream().reduce(0L, (s, i) -> s + i);
        System.out.printf("somme séquentielle du Stream<Integer> : somme=%s, durée (ms)=%s%n", somme, new Date().getTime() - début);
    }
}
  • السطر 22: حساب مجموع تيار من الأرقام Long؛

تم الحصول على النتائج التالية:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4537 ms
somme séquentielle du Stream<Integer> : somme=49999995000000, durée (ms)=226

الآن، دعونا نستبدل السطر 22 بما يلي (مثال 06i2):


long somme = nombres.stream().mapToLong(n -> n.longValue()).sum();

تسمح لنا طريقة Stream<Integer>.mapToLong بالحصول على LongStream من العناصر الأولية من نوع long، والتي نقوم بعد ذلك بجمعها باستخدام دالة sum. ثم نحصل على النتائج التالية:

1
2
3
La JVM a détecté [8] coeurs sur votre machine
création de la liste des 10000000 nombres en 4511 ms
somme séquentielle du LongStream : somme=49999995000000, durée (ms)=99

مكاسب الأداء واضحة.

5.6.13. طرق تدفق الأرقام الأولية


// max d'un flux de int
affiche("IntStream max", IntStream.of(1, 10, 8).max());
// min d'un flux de double
affiche("DoubleStream min", DoubleStream.of(1.5, 10.4, 8.9).min());
// moyenne d'un flux de int
affiche("IntStream average", IntStream.of(1, 10, 8).average().getAsDouble());
// statistiques d'un flux de int
affiche("IntStream summaryStatistics", IntStream.of(1, 10, 8).summaryStatistics());

توفر تدفقات القيم الأولية (int، long، double) طرقًا مصممة خصيصًا لهذه الأنواع. ونتيجة تنفيذ الكود السابق هي كما يلي:

1
2
3
4
5
6
7
8
IntStream max ----
{"asInt":10,"present":true}
DoubleStream min ----
{"asDouble":1.5,"present":true}
IntStream average ----
6.333333333333333
IntStream summaryStatistics ----
{"count":3,"sum":19,"min":1,"max":10,"average":6.333333333333333}
  • نتيجة السطر 2 من الكود هي نوع OptionalInt مشابه لنوع Optional<Integer>. يمكن استرداد القيمة المخزنة في هذا الكائن باستخدام الطريقة [getAsInt()]. يمكن التحقق من وجود قيمة باستخدام الطريقة [isPresent()]. لا يعني السطر 2 من النتائج أن فئة [OptionalInt] تحتوي على حقول باسم [asInt] و [present]. بشكل افتراضي، تستخدم مكتبة JSON جميع طرق getX و isY العامة للكائن المراد تسلسله إلى JSON. وهنا، توجد بالفعل طريقة [getAsInt] وطريقة أخرى [isPresent]، على الرغم من أن الحقول [asInt، present] نفسها غير موجودة؛
  • نتيجة السطر 4 من الكود هي نوع OptionalDouble مشابه لنوع Optional<Double>؛
  • نتيجة السطر 6 من الكود هي نوع OptionalDouble يمكن الحصول على قيمته باستخدام طريقة [getAsDouble()]. تحسب طريقة [average] متوسط تيار الأرقام؛
  • نتيجة السطر 8 من الكود هي نوع IntSummaryStatistics المُعرَّف على النحو التالي:
 

يمكننا أن نرى أن كائن IntSummaryStatistics الناتج يوفر إحصائيات متنوعة حول دفق الأرقام، مثل عدد القيم والمجموع والقيمة القصوى والقيمة الدنيا والمتوسط.