מדריך API 8 של Java 8 Stream

1. סקירה כללית

במדריך מעמיק זה נעבור על השימוש המעשי ב- Java 8 Streams מיצירה ועד ביצוע מקביל.

כדי להבין חומר זה, על הקוראים להיות בעלי ידע בסיסי ב- Java 8 (ביטויים למבדה, אופציונאלי, הפניות לשיטה) וממשק ה- API של Stream. אם אינך מכיר את הנושאים הללו, אנא עיין במאמרים הקודמים שלנו - תכונות חדשות ב- Java 8 ומבוא ל- Java 8 Streams.

2. יצירת זרמים

ישנן דרכים רבות ליצור מופע זרם של מקורות שונים. לאחר שנוצר, המופע לא ישנה את מקורו, ולכן מאפשר יצירת מספר מופעים ממקור יחיד.

2.1. זרם ריק

ה ריק() יש להשתמש בשיטה במקרה של יצירת זרם ריק:

זרם streamEmpty = Stream.empty ();

זה לעתים קרובות המקרה כי ריק() נעשה שימוש בשיטה עם היצירה כדי למנוע חזרה ריק לזרמים ללא אלמנט:

זרם ציבורי streamOf (רשימת רשימות) רשימת החזרה == null 

2.2. זרם של אוסף

ניתן ליצור זרם גם מכל סוג אוסף (אוסף, רשימה, סט):

אוסף אוסף = Arrays.asList ("a", "b", "c"); זרם streamOfCollection = collection.stream ();

2.3. זרם מערך

מערך יכול להיות גם מקור לזרם:

זרם streamOfArray = Stream.of ("a", "b", "c");

ניתן ליצור אותם גם ממערך קיים או מחלק ממערך:

מחרוזת [] arr = מחרוזת חדשה [] {"a", "b", "c"}; זרם streamOfArrayFull = Arrays.stream (arr); זרם streamOfArrayPart = Arrays.stream (arr, 1, 3);

2.4. Stream.builder ()

כאשר משתמשים בבנאי יש לציין את הסוג הרצוי בחלק הימני של ההצהרה, אחרת ה לִבנוֹת() השיטה תיצור מופע של זרם:

זרם streamBuilder = Stream.builder (). הוסף ("a"). הוסף ("b"). הוסף ("c"). Build ();

2.5. Stream.generate ()

ה לִיצוֹר() השיטה מקבלת א ספק לדור אלמנטים. מכיוון שהזרם שנוצר הוא אינסופי, על המפתח לציין את הגודל הרצוי או את לִיצוֹר() השיטה תפעל עד שתגיע למגבלת הזיכרון:

זרם streamGenerated = Stream.generate (() -> "אלמנט"). מגבלה (10);

הקוד שלמעלה יוצר רצף של עשרה מחרוזות עם הערך - "אֵלֵמֶנט".

2.6. Stream.iterate ()

דרך נוספת ליצור זרם אינסופי היא באמצעות ה- לְחַזֵר() שיטה:

זרם streamIterated = Stream.iterate (40, n -> n + 2). Limit (20);

האלמנט הראשון של הזרם שנוצר הוא פרמטר ראשון של ה- לְחַזֵר() שיטה. ליצירת כל אלמנט הבא הפונקציה שצוינה מוחלת על האלמנט הקודם. בדוגמה שלמעלה האלמנט השני יהיה 42.

2.7. זרם פרימיטיבי

Java 8 מציע אפשרות ליצור זרמים משלושה סוגים פרימיטיביים: int, ארוך ו לְהַכפִּיל. כפי ש זרם הוא ממשק כללי ואין דרך להשתמש בפרימיטיבים כפרמטר מסוג עם כללי, נוצרו שלושה ממשקים מיוחדים חדשים: IntStream, LongStream, DoubleStream.

השימוש בממשקים החדשים מקל על איגרוף אוטומטי מיותר מאפשר פרודוקטיביות מוגברת:

IntStream intStream = IntStream.range (1, 3); LongStream longStream = LongStream.rangeClosed (1, 3);

ה טווח (int startInclusive, int endExclusive) השיטה יוצרת זרם מסודר מהפרמטר הראשון לפרמטר השני. זה מגדיל את הערך של האלמנטים הבאים כאשר הצעד שווה ל- 1. התוצאה אינה כוללת את הפרמטר האחרון, אלא רק גבול עליון של הרצף.

ה rangeClosed (int startInclusive, int endInclusive)השיטה עושה את אותו הדבר עם הבדל אחד בלבד - האלמנט השני כלול. ניתן להשתמש בשתי שיטות אלה ליצירת כל אחד משלושת סוגי הזרמים הראשוניים.

מאז Java 8 את אַקרַאִי class מספק מגוון רחב של שיטות לייצור זרמים של פרימיטיבים. לדוגמה, הקוד הבא יוצר a DoubleStream, שיש בו שלושה אלמנטים:

אקראי אקראי = אקראי חדש (); DoubleStream doubleStream = random.doubles (3);

2.8. זרם של חוּט

חוּט יכול לשמש גם כמקור ליצירת זרם.

בעזרת ה- תווים () שיטת ה- חוּט מעמד. מכיוון שאין ממשק CharStream ב- JDK, ה- IntStream משמש לייצוג זרם של תווים במקום.

IntStream streamOfChars = "abc" .chars ();

הדוגמה הבאה שוברת א חוּט למחרוזות משנה לפי המפורט RegEx:

זרם streamOfString = Pattern.compile (","). SplitAsStream ("a, b, c");

2.9. זרם קבצים

מחלקת Java NIO קבצים מאפשר ליצור זרם של קובץ טקסט דרך ה- שורות() שיטה. כל שורת טקסט הופכת לרכיב בזרם:

נתיב נתיב = Paths.get ("C: \ file.txt"); זרם streamOfStrings = Files.lines (נתיב); זרם streamWithCharset = Files.lines (נתיב, Charset.forName ("UTF-8"));

ה ערכת ניתן לציין כטיעון של שורות() שיטה.

3. הפניה לזרם

אפשר ליצור זרם ולקבל התייחסות נגישה אליו כל עוד נקראו רק פעולות ביניים. ביצוע פעולת מסוף הופך זרם לנגיש.

כדי להדגים זאת נשכח לזמן מה שהשיטה הטובה ביותר היא שרשרת פעולות שרשרת. מלבד מילוליותו המיותרת, מבחינה טכנית הקוד הבא תקף:

זרם זרם = Stream.of ("a", "b", "c"). פילטר (אלמנט -> element.contains ("b")); אופציונלי anyElement = stream.findAny ();

אך ניסיון לעשות שימוש חוזר באותה התייחסות לאחר קריאה לפעולת המסוף יפעיל את IllegalStateException:

FirstElement אופציונלי = stream.findFirst ();

כמו ה IllegalStateException הוא חריגת זמן ריצה, מהדר לא יסמן על בעיה. לכן, חשוב מאוד לזכור זאת ג'אווה 8 לא ניתן לעשות שימוש חוזר בזרמים.

סוג זה של התנהגות הוא הגיוני מכיוון שזרמים נועדו לספק יכולת להחיל רצף סופי של פעולות על מקור האלמנטים בסגנון פונקציונלי, אך לא כדי לאחסן אלמנטים.

לכן, כדי לגרום לקוד הקודם לעבוד כראוי יש לבצע שינויים מסוימים:

אלמנטים ברשימה = Stream.of ("a", "b", "c"). פילטר (element -> element.contains ("b")) .collect (Collectors.toList ()); אופציונלי anyElement = elements.stream (). FindAny (); FirstElement אופציונלי = elements.stream (). FindFirst ();

4. זרם צינור

כדי לבצע רצף פעולות על אלמנטים של מקור הנתונים ולצבור את תוצאותיהם, יש צורך בשלושה חלקים - ה- מָקוֹר, פעולות ביניים ו הפעלת מסוף.

פעולות ביניים מחזירות זרם שונה. לדוגמא, כדי ליצור זרם חדש של הקיים ללא מעט אלמנטים לדלג() יש להשתמש בשיטה:

זרם onceModifiedStream = Stream.of ("abcd", "bbcd", "cbcd"). דלג על (1);

אם יש צורך ביותר משינוי אחד, ניתן לשרשר פעולות ביניים. נניח שעלינו להחליף גם כל אלמנט זרם זרם עם מחרוזת משנה של כמה עיצובים ראשונים. זה ייעשה על ידי שרשור לדלג() וה מַפָּה() שיטות:

זרם פעמיים ModifiedStream = stream.skip (1) .map (element -> element.substring (0, 3));

כפי שאתה יכול לראות, מַפָּה() השיטה לוקחת ביטוי למבדה כפרמטר. אם אתה רוצה ללמוד עוד על lambdas, עיין במדריך שלנו Lambda ביטויים וממשקים פונקציונליים: טיפים ושיטות עבודה מומלצות.

זרם כשלעצמו הוא חסר ערך, הדבר האמיתי שמשתמש מעוניין בו הוא תוצאה של פעולת הטרמינל, שיכולה להיות ערך מסוג כלשהו או פעולה המיושמת על כל רכיב בזרם. ניתן להשתמש בפעולת מסוף אחת בלבד בכל זרם.

הדרך הנכונה והנוחה ביותר להשתמש בזרמים היא על ידי צינור זרם, שהוא שרשרת של מקור זרם, פעולות ביניים ופעולה סופנית. לדוגמה:

רשימת רשימה = Arrays.asList ("abc1", "abc2", "abc3"); גודל ארוך = list.stream (). דלג (1) .map (element -> element.substring (0, 3)). ממוינים (). count ();

5. קריאה עצלה

פעולות ביניים עצלות. זה אומר ש הם יופעלו רק אם יהיה צורך בביצוע פעולת המסוף.

כדי להדגים זאת, דמיין שיש לנו שיטה נקרא(), שמגדיל דלפק פנימי בכל פעם שהוא נקרא:

דלפק ארוך פרטי; חלל פרטי היה נקרא () {counter ++; }

בוא נקרא שיטה הייתהשקוראים לו() מההפעלה לְסַנֵן():

רשימת רשימה = Arrays.asList ("abc1", "abc2", "abc3"); מונה = 0; זרם זרם = list.stream (). Filter (element -> {wasCalled (); return element.contains ("2");});

מכיוון שיש לנו מקור לשלושה אלמנטים אנו יכולים להניח שיטה זו לְסַנֵן() יקרא שלוש פעמים והערך של דֶלְפֵּק משתנה יהיה 3. אך הפעלת קוד זה אינה משתנה דֶלְפֵּק בכלל, זה עדיין אפס, אז, לְסַנֵן() השיטה לא נקראה אפילו פעם אחת. הסיבה מדוע - חסר פעולת הטרמינל.

בואו נשכתב קצת את הקוד הזה על ידי הוספת a מַפָּה() הפעלה והפעלה סופנית - findFirst (). נוסיף גם יכולת לעקוב אחר סדר שיחות שיטה בעזרת רישום:

זרם אופציונלי = list.stream (). Filter (element -> {log.info ("filter () נקרא"); return element.contains ("2");}). Map (element -> {log.info ("map () נקרא"); return element.toUpperCase ();}). findFirst ();

יומן התוצאה מראה כי ה- לְסַנֵן() השיטה נקראה פעמיים מַפָּה() שיטה רק פעם אחת. הסיבה לכך היא כי הצינור מתבצע בצורה אנכית. בדוגמה שלנו האלמנט הראשון בזרם לא סיפק את פרדיקט המסנן, ואז את לְסַנֵן() השיטה הופעלה עבור האלמנט השני שעבר את המסנן. בלי להתקשר ל לְסַנֵן() עבור אלמנט שלישי ירדנו דרך הצינור אל מַפָּה() שיטה.

ה findFirst () הפעולה מספקת רק על ידי אלמנט אחד. לכן, בדוגמה המסוימת הזו, קריאה עצלה אפשרה להימנע משתי שיחות שיטה - אחת עבור ה- לְסַנֵן() ואחד עבור ה- מַפָּה().

6. צו ההוצאה לפועל

מנקודת מבט הביצועים, הסדר הנכון הוא אחד ההיבטים החשובים ביותר של פעולות שרשור בצינור הזרם:

long size = list.stream (). map (element -> {wasCalled (); return element.substring (0, 3);}). דלג (2) .count ();

ביצוע קוד זה יגדיל את ערך הדלפק בשלושה. משמעות הדבר היא כי מַפָּה() שיטת הזרם נקראה שלוש פעמים. אבל הערך של גודל אחד. אז לזרם שנוצר יש רק אלמנט אחד והוצאנו את היקר מַפָּה() פעולות ללא סיבה פעמיים מתוך שלוש.

אם נשנה את סדר ה- לדלג() וה מַפָּה() שיטות, ה דֶלְפֵּק יגדל רק באחד. אז, השיטה מַפָּה() ייקרא רק פעם אחת:

גודל ארוך = list.stream (). דלג על (2) .map (element -> {wasCalled (); return element.substring (0, 3);}). count ();

זה מעלה אותנו לכלל: יש למקם פעולות ביניים המפחיתות את גודל הזרם לפני פעולות החלות על כל אלמנט. אז שמור על שיטות כמו skip (), filter (), נבדל () בראש צינור הזרם שלך.

7. הפחתת זרמים

ל- API פעולות מסוף רבות המאגדות זרם לסוג או לפרימיטיבי, למשל, ספירה (), מקסימום (), דקה (), סכום (), אך פעולות אלה פועלות בהתאם ליישום המוגדר מראש. ומה אם מפתח צריך להתאים אישית את מנגנון ההפחתה של Stream? ישנן שתי שיטות המאפשרות לעשות זאת - לְהַפחִית()וה לאסוף() שיטות.

7.1. ה לְהַפחִית() שיטה

ישנן שלוש וריאציות של שיטה זו, אשר נבדלות על פי חתימותיהן וסוגיהן החוזרים. הם יכולים לכלול את הפרמטרים הבאים:

זהות - הערך ההתחלתי עבור מצבר או ערך ברירת מחדל אם זרם ריק ואין מה לצבור;

מצבר - פונקציה המציינת לוגיקה של צבירת אלמנטים. כאשר המצבר יוצר ערך חדש לכל שלב של צמצום, כמות הערכים החדשים שווה לגודל הזרם ורק הערך האחרון שימושי. זה לא טוב במיוחד לביצועים.

קומבינר - פונקציה שמאגדת תוצאות של המצבר. Combiner נקרא רק במצב מקביל להפחתת תוצאות מצברים מחוטים שונים.

אז בואו נסתכל על שלוש השיטות הבאות:

OptionalInt מופחת = IntStream.range (1, 4). להפחית ((a, b) -> a + b);

מוּפחָת = 6 (1 + 2 + 3)

int מופחתTwoParams = IntStream.range (1, 4) .פחת (10, (a, b) -> a + b);

מופחתתTwoParams = 16 (10 + 1 + 2 + 3)

int מופחתתParams = Stream.of (1, 2, 3) .פחת (10, (a, b) -> a + b, (a, b) -> {log.info ("שילב נקרא"); להחזיר a + ב;});

התוצאה תהיה זהה לזו שבדוגמה הקודמת (16) ולא יהיה כניסה שמשמעותה, לא נקרא אותו שילוב. כדי לגרום לשילוב לעבוד, זרם צריך להיות מקביל:

int reducedParallel = Arrays.asList (1, 2, 3). ParallelStream () .reduce (10, (a, b) -> a + b, (a, b) -> {log.info ("combiner נקרא" ); להחזיר a + b;});

התוצאה כאן שונה (36) ולקומבינר קראו פעמיים. כאן ההפחתה עובדת על ידי האלגוריתם הבא: המצבר רץ שלוש פעמים על ידי הוספת כל רכיב בזרם ל זהות לכל אלמנט הזרם. פעולות אלה נעשות במקביל. כתוצאה מכך, יש להם (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;). כעת קומבינר יכול למזג את שלוש התוצאות הללו. זה צריך שתי איטרציות בשביל זה (12 + 13 = 25; 25 + 11 = 36).

7.2. ה לאסוף() שיטה

הפחתת זרם יכולה להתבצע גם על ידי פעולת מסוף אחרת - לאסוף() שיטה. הוא מקבל טיעון מהסוג אַסְפָן, המפרט את מנגנון ההפחתה. ישנם כבר אספנים שהוגדרו מראש עבור הפעולות הנפוצות ביותר. ניתן לגשת אליהם בעזרת ה- אספנים סוּג.

בחלק זה נשתמש בהמשך רשימה כמקור לכל הזרמים:

רשימת productList = Arrays.asList (מוצר חדש (23, "תפוחי אדמה"), מוצר חדש (14, "כתום"), מוצר חדש (13, "לימון"), מוצר חדש (23, "לחם"), מוצר חדש ( 13, "סוכר"));

המרת זרם ל אוסף (אוסף, רשימה אוֹ מַעֲרֶכֶת):

רשימה collectorCollection = productList.stream (). מפה (Product :: getName) .collect (Collectors.toList ());

הפחתה ל חוּט:

מחרוזת listToString = productList.stream (). מפה (מוצר :: getName) .collect (Collectors.joining (",", "[", "]"));

ה נַגָר() לשיטה יכול להיות בין אחד לשלושה פרמטרים (מפריד, קידומת, סיומת). הדבר הכי נוח בשימוש נַגָר() - מפתח אינו צריך לבדוק אם הזרם מגיע לסופו כדי להחיל את הסיומת ולא להחיל תוחם. אַסְפָן ידאג לזה.

עיבוד הערך הממוצע של כל האלמנטים המספריים של הזרם:

כפול ממוצע מחיר = productList.stream () .collect (Collectors.averagingInt (מוצר :: getPrice));

עיבוד סכום כל האלמנטים המספריים של הזרם:

int summingPrice = productList.stream () .collect (Collectors.summingInt (Product :: getPrice));

שיטות averagingXX (), summingXX () ו סיכום XX () יכול לעבוד כמו עם פרימיטיבים (int, ארוך, כפול) כמו בשיעורי העטיפה שלהם (שלם, ארוך, כפול). תכונה אחת חזקה יותר של שיטות אלה היא לספק את המיפוי. לכן, מפתח אינו צריך להשתמש בתוסף נוסף מַפָּה() פעולה לפני לאסוף() שיטה.

איסוף מידע סטטיסטי על מרכיבי הזרם:

נתונים סטטיסטיים של IntSummaryStatistics = productList.stream () .collect (Collectors.summarizingInt (Product :: getPrice));

באמצעות המופע שנוצר מסוג IntSummaryStatistics מפתח יכול ליצור דוח סטטיסטי על ידי יישום toString () שיטה. התוצאה תהיה חוּט המשותף לזה "IntSummaryStatistics {count = 5, sum = 86, min = 13, average = 17,200000, max = 23}".

קל גם לחלץ מאובייקט זה ערכים נפרדים עבור ספירה, סכום, דקות, ממוצע על ידי יישום שיטות getCount (), getSum (), getMin (), getAverage (), getMax (). ניתן לחלץ את כל הערכים הללו מצינור יחיד.

קיבוץ אלמנטים של זרם לפי הפונקציה שצוינה:

מַפָּה collectorMapOfLists = productList.stream () .collect (Collectors.groupingBy (Product :: getPrice));

בדוגמה שלמעלה הזרם הצטמצם ל מַפָּה שמקבץ את כל המוצרים לפי מחירם.

חלוקת רכיבי הזרם לקבוצות על פי פרדיקט כלשהו:

מַפָּה mapPartioned = productList.stream () .collect (Collectors.partitioningBy (element -> element.getPrice ()> 15));

דוחפים את האספן לבצע טרנספורמציה נוספת:

הגדר unmodifiableSet = productList.stream () .collect (Collectors.collectingAndThen (Collectors.toSet (), אוספים :: unmodifiableSet));

במקרה הספציפי הזה, האספן המיר זרם ל- a מַעֲרֶכֶת ואז יצר את הבלתי ניתן לשינוי מַעֲרֶכֶת מחוץ לזה.

אספן מותאם אישית:

אם מסיבה כלשהי, צריך ליצור אספן מותאם אישית, הדרך הקלה ביותר והפחות מילולית לעשות זאת - היא להשתמש בשיטה שֶׁל() מהסוג אַסְפָן.

אַסְפָן toLinkedList = Collector.of (LinkedList :: new, LinkedList :: add, (first, second) -> {first.addAll (second); first return;}); LinkedList linkedListOfPersons = productList.stream (). Collect (toLinkedList);

בדוגמה זו, מופע של אַסְפָן הצטמצם ל רשימה מקושרת.

זרמים מקבילים

לפני Java 8, ההקבלה הייתה מורכבת. מתהווה של שירות ExecutorService וה ForkJoin פשטו את חיי המפתח מעט, אך הם עדיין צריכים לזכור כיצד ליצור מנהל ספציפי, כיצד לנהל אותו וכן הלאה.ג'אווה 8 הציגה דרך להשגת מקבילות בסגנון פונקציונלי.

ה- API מאפשר יצירת זרמים מקבילים, המבצעים פעולות במצב מקביל. כאשר מקור הזרם הוא א אוסף או מַעֲרָך ניתן להשיג זאת בעזרת parallelStream () שיטה:

זרם streamOfCollection = productList.parallelStream (); isParallel בוליאני = streamOfCollection.isParallel (); bigPrice בוליאני = streamOfCollection .map (מוצר -> product.getPrice () * 12) .anyMatch (מחיר -> מחיר> 200);

אם מקור הזרם הוא משהו שונה מ- אוסף או מַעֲרָך, ה מַקְבִּיל() יש להשתמש בשיטה:

IntStream intStreamParallel = IntStream.range (1, 150) .parallel (); isParallel בוליאני = intStreamParallel.isParallel ();

מתחת למכסה המנוע, Stream API משתמש אוטומטית ב- ForkJoin מסגרת לביצוע פעולות במקביל. כברירת מחדל, נעשה שימוש במאגר האשכולות המשותף ואין שום דרך (לפחות בינתיים) להקצות לו מאגר אשכולות מותאם אישית. ניתן להתגבר על זה באמצעות קבוצה מותאמת אישית של אספנים מקבילים.

כשמשתמשים בזרמים במצב מקביל, הימנע מחסימת פעולות והשתמש במצב מקביל כאשר למשימות נדרש זמן דומה לביצוע (אם משימה אחת נמשכת הרבה יותר מהשנייה, היא יכולה להאט את זרימת העבודה של האפליקציה המלאה).

ניתן להמיר את הזרם במצב מקביל למצב רציף באמצעות סִדרָתִי() שיטה:

IntStream intStreamSequential = intStreamParallel.sequential (); isParallel בוליאני = intStreamSequential.isParallel ();

מסקנות

ממשק ה- API של Stream הוא מערכת כלים עוצמתית אך פשוטה להבנה לעיבוד רצף של אלמנטים. זה מאפשר לנו להפחית כמות עצומה של קוד boilerplate, ליצור תוכניות קריאות יותר ולשפר את התפוקה של האפליקציה כאשר משתמשים בה נכון.

ברוב דגימות הקוד המוצגות במאמר זה זרמים נותרו ללא צריכה (לא יישמנו את ה- סגור() שיטה או פעולת מסוף). באפליקציה אמיתית, אל תשאיר זרמים מיוצרים לא נצרכים מכיוון שזה יוביל לדליפות זיכרון.

דוגמאות הקוד המלאות הנלוות למאמר זמינות באתר GitHub.