מדריך לאספנים של Java 8

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

במדריך זה נעבור על אספני Java 8 המשמשים בשלב הסופי של עיבוד א זרם.

אם אתה רוצה לקרוא עוד על זרם API עצמו, בדוק מאמר זה.

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

2. ה Stream.collect () שיטה

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

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

3. אספנים

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

ייבא java.util.stream.Collectors סטטי. *;

או רק אספני יבוא יחיד לבחירתך:

ייבא java.util.stream.Collectors.toList סטטי; ייבא java.util.stream.Collectors.toMap סטטי; ייבא java.util.stream.Collectors.toSet סטטי;

בדוגמאות הבאות נשתמש שוב ברשימה הבאה:

רשימה givenList = Arrays.asList ("a", "bb", "ccc", "dd");

3.1. Collectors.toList ()

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

בואו ניצור זרם מופע המייצג רצף של אלמנטים ואוסף אותם ל- רשימה למשל:

תוצאת רשימה = givenList.stream () .collect (toList ());

3.1.1. Collectors.toUnmodifiableList ()

Java 10 הציג דרך נוחה לצבור את זרם אלמנטים לבלתי ניתנים לשינוי רשימה:

תוצאת רשימה = givenList.stream () .collect (toUnmodifiableList ());

אם ננסה לשנות את ה- תוֹצָאָהרשימה, נקבל לא נתמךOperationException:

assertThatThrownBy (() -> result.add ("foo")) .isInstanceOf (UnsupportedOperationException.class);

3.2. Collectors.toSet ()

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

בואו ניצור זרם מופע המייצג רצף של אלמנטים ואוסף אותם ל- מַעֲרֶכֶת למשל:

הגדר תוצאה = givenList.stream () .collect (toSet ());

א מַעֲרֶכֶת אינו מכיל אלמנטים כפולים. אם האוסף שלנו מכיל אלמנטים השווים זה לזה, הם מופיעים בתוצאה מַעֲרֶכֶת רק פעם אחת:

List listWithDuplicates = Arrays.asList ("a", "bb", "c", "d", "bb"); הגדר תוצאה = listWithDuplicates.stream (). Collect (toSet ()); assertThat (תוצאה) .hasSize (4);

3.2.1. Collectors.toUnmodifiableSet ()

מכיוון שג'אווה 10 אנו יכולים ליצור ללא שינוי מַעֲרֶכֶת באמצעות toUnmodifiableSet () אַסְפָן:

הגדר תוצאה = givenList.stream () .collect (toUnmodifiableSet ());

כל ניסיון לשנות את תוצאת סט בסופו של דבר עם לא נתמךOperationException:

assertThatThrownBy (() -> result.add ("foo")) .isInstanceOf (UnsupportedOperationException.class);

3.3. Collectors.toCollection ()

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

בואו ניצור זרם מופע המייצג רצף של אלמנטים ואוסף אותם ל- רשימה מקושרת למשל:

תוצאת רשימה = givenList.stream () .collect (toCollection (LinkedList :: new))

שימו לב שהדבר לא יעבוד עם אוספים בלתי ניתנים לשינוי. במקרה כזה, יהיה עליך לכתוב מותאם אישית אַסְפָן יישום או שימוש איסוף ואז.

3.4. אספנים.למפות()

למפות אספן יכול לשמש לאיסוף זרם אלמנטים לתוך a מַפָּה למשל. לשם כך עלינו לספק שתי פונקציות:

  • keyMapper
  • valueMapper

keyMapper ישמש לחילוץ א מַפָּה מפתח מתוך זרם אלמנט, ו valueMapper ישמש לחילוץ ערך המשויך למפתח נתון.

בואו נאסוף את האלמנטים האלה לא מַפָּה השומר מחרוזות כמפתחות ואורכן כערכים:

תוצאת מפה = givenList.stream () .collect (toMap (Function.identity (), מחרוזת :: אורך))

Function.identity () הוא רק קיצור דרך להגדרת פונקציה המקבלת ומחזירה את אותו הערך.

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

List listWithDuplicates = Arrays.asList ("a", "bb", "c", "d", "bb"); assertThatThrownBy (() -> {listWithDuplicates.stream (). collect (toMap (Function.identity (), String :: length));}). isInstanceOf (IllegalStateException.class);

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

במקרים כאלה עם התנגשות מפתח, עלינו להשתמש למפות עם חתימה אחרת:

תוצאת מפה = givenList.stream () .collect (toMap (Function.identity (), מחרוזת :: אורך, (פריט, identItem) -> פריט));

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

3.4.1. Collectors.toUnmodifiableMap ()

באופן דומה לגבי רשימהs ו- מַעֲרֶכֶתs, Java 10 הציגה דרך קלה לאיסוף זרם אלמנטים לבלתי ניתנים לשינוי מַפָּה:

תוצאת מפה = givenList.stream () .collect (toMap (Function.identity (), מחרוזת :: אורך))

כפי שאנו רואים, אם ננסה להכניס ערך חדש ל- a תוצאה מפה, אנחנו נקבל לא נתמךOperationException:

assertThatThrownBy (() -> result.put ("foo", 3)) .isInstanceOf (UnsupportedOperationException.class);

3.5. אספנים.collectingAndThen ()

Collecting AndThen הוא אספן מיוחד המאפשר ביצוע פעולה נוספת על תוצאה מיד לאחר סיום האיסוף.

בואו נאסוף זרם אלמנטים לא רשימה ואז להמיר את התוצאה ל- ImmutableList למשל:

תוצאת רשימה = givenList.stream () .collect (collectionAndThen (toList (), ImmutableList :: copyOf))

3.6. אספנים.jמשחה ()

הִצטָרְפוּת אספן יכול לשמש להצטרפות זרם אלמנטים.

אנו יכולים להצטרף אליהם על ידי ביצוע:

תוצאת מחרוזת = givenList.stream () .collect (joining ());

אשר יביא ל:

"abbcccdd"

ניתן גם לציין מפרידים, קידומות, ותיקונים מותאמים אישית:

תוצאת מחרוזת = givenList.stream () .collect (joining (""));

אשר יביא:

"bb ccc dd"

או שאתה יכול לכתוב:

תוצאת מחרוזת = givenList.stream () .collect (הצטרפות ("", "PRE-", "-POST"));

אשר יביא:

"PRE-a bb ccc dd-POST"

3.7. אספנים.cמשמיץ ()

סְפִירָה הוא אספן פשוט המאפשר פשוט לספור את כולם זרם אלמנטים.

עכשיו נוכל לכתוב:

תוצאה ארוכה = givenList.stream () .collect (ספירה ());

3.8. אספנים.summarizingDouble / Long / Int ()

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

אנו יכולים להשיג מידע על אורכי מחרוזות על ידי ביצוע:

DoubleSummaryStatistics result = givenList.stream () .collect (summarizingDouble (String :: length));

במקרה זה הדברים הבאים יהיו נכונים:

assertThat (result.getAverage ()). isEqualTo (2); assertThat (result.getCount ()). isEqualTo (4); assertThat (result.getMax ()). isEqualTo (3); assertThat (result.getMin ()). isEqualTo (1); assertThat (result.getSum ()). isEqualTo (8);

3.9. Collectors.averagingDouble / Long / Int ()

ממוצע כפול / ארוך / Int הוא אספן שפשוט מחזיר ממוצע של אלמנטים שחולצו.

אנו יכולים לקבל אורך מחרוזות ממוצע על ידי ביצוע:

תוצאה כפולה = givenList.stream () .collect (averagingDouble (מחרוזת :: אורך));

3.10. אספנים.summing כפול / ארוך / Int ()

SummingDouble / Long / Int הוא אספן שפשוט מחזיר סכום של אלמנטים שחולצו.

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

תוצאה כפולה = givenList.stream () .collect (summingDouble (מחרוזת :: אורך));

3.11. Collectors.maxBy () / minBy ()

מקס ביי/MinBy אספנים מחזירים את האלמנט הגדול / הקטן ביותר של זרם על פי תנאי משווה למשל.

אנו יכולים לבחור את האלמנט הגדול ביותר על ידי ביצוע:

תוצאה אופציונלית = givenList.stream () .collect (maxBy (Comparator.naturalOrder ()));

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

3.12. אספנים.groupingBy ()

קיבוץ לפי אספן משמש לקיבוץ אובייקטים על ידי נכס כלשהו ואחסון התוצאות ב- מַפָּה למשל.

אנחנו יכולים לקבץ אותם לפי אורך מחרוזת ולאחסן תוצאות קיבוץ מַעֲרֶכֶת מקרים:

מַפָּה תוצאה = givenList.stream () .collect (groupingBy (מחרוזת :: אורך, toSet ()));

זה יביא לכך שהדברים נכונים:

assertThat (תוצאה) .containsEntry (1, newHashSet ("a")) .containsEntry (2, newHashSet ("bb", "dd")) .containsEntry (3, newHashSet ("ccc")); 

שימו לב כי הטיעון השני של קיבוץ לפי השיטה היא א אַסְפָן ואתה חופשי להשתמש בכל אַסְפָן לבחירתך.

3.13. Collectors.partitioningBy ()

PartitioningBy הוא מקרה מיוחד של קיבוץ לפי שמקבל א לְבַסֵס מופע ואוסף זרם אלמנטים לתוך a מַפָּה מקרה שחנויות בוליאני ערכים כמפתחות ואוספים כערכים. תחת המקש "נכון", אתה יכול למצוא אוסף של אלמנטים התואמים את הנתון לְבַסֵסומתחת למפתח "שקר", תוכלו למצוא אוסף של אלמנטים שאינם תואמים את הנתון לְבַסֵס.

אתה יכול לכתוב:

מַפָּה תוצאה = givenList.stream () .collect (partitioningBy (s -> s.length ()> 2))

מה שמביא למפה המכילה:

{false = ["a", "bb", "dd"], true = ["ccc"]} 

3.14. Collectors.teeing ()

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

מספרי רשימה = Arrays.asList (42, 4, 2, 24); אופציונלי min = numbers.stream (). Collect (minBy (Integer :: CompareTo)); אופציונלי max = numbers.stream (). Collect (maxBy (Integer :: CompareTo)); // לעשות משהו שימושי עם מינימום ומקסימום

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

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

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

numbers.stream (). collect (teeing (minBy (Integer :: CompareTo), // אספן הראשון maxBy (Integer :: CompareTo), // האספן השני (min, max) -> // מקבל את התוצאה מאלה אספנים ומשלב אותם));

דוגמה זו זמינה ב- GitHub בפרויקט core-java-12.

4. אספנים בהתאמה אישית

אם אתה רוצה לכתוב את יישום ה- Collector שלך, עליך ליישם את ממשק Collector ולציין את שלושת הפרמטרים הגנריים שלו:

אספן ממשק ציבורי {...}
  1. ט - סוג האובייקטים שיהיו זמינים לאיסוף,
  2. א - סוג אובייקט הצבר המשתנה,
  3. ר - סוג התוצאה הסופית.

בואו לכתוב דוגמה אספן לאיסוף אלמנטים ל- ImmutableSet למשל. ראשית אנו מציינים את הסוגים הנכונים:

מחלקה פרטית ImmutableSetCollector מיישם אספן {...}

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

במקרה זה, נמשיך עם ImmutableSet.Builder ועכשיו עלינו ליישם 5 שיטות:

  • ספקספק()
  • BiConsumerמַצבֵּר()
  • BinaryOperatorקומבינר()
  • פוּנקצִיָהגימור()
  • מַעֲרֶכֶת מאפיינים()

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

הספק הציבורי של @Override ספק () {החזר ImmutableSet :: בונה; } 

המצבר () השיטה מחזירה פונקציה המשמשת להוספת אלמנט חדש לקיים מַצבֵּר אובייקט, אז בואו פשוט נשתמש ב- בּוֹנֶהשל לְהוֹסִיף שיטה.

@Override ציבורי BiConsumer מצבר () {החזר ImmutableSet.Builder :: הוסף; }

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

@Override BinaryOperator ציבורי combiner () {return (left, right) -> left.addAll (right.build ()); }

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

פונקציה ציבורית @Override גימור () {return ImmutableSet.Builder :: build; }

המאפיינים () השיטה משמשת כדי לספק ל- Stream מידע נוסף שישמש לאופטימיזציות פנימיות. במקרה זה, אנו לא שמים לב לסדר האלמנטים ב- a מַעֲרֶכֶת כדי שנשתמש מאפיינים. לא מסודרים. לקבלת מידע נוסף בנושא זה, בדוק מאפיינים'JavaDoc.

@Override public הגדר מאפיינים () {return Sets.immutableEnumSet (Characteristics.UNORDERED); }

הנה היישום המלא יחד עם השימוש:

מחלקה ציבורית ImmutableSetCollector מיישם אספן {@ הספק ציבורי לניהול ספק () {החזר ImmutableSet :: בונה; } @Override BiConsumer ציבורי מצבר () {החזר ImmutableSet.Builder :: הוסף; } @Override BinaryOperator ציבורי combiner () {return (left, right) -> left.addAll (right.build ()); } פונקציה ציבורית @Override גימור () {return ImmutableSet.Builder :: build; } @Override public הגדר מאפיינים () {return Sets.immutableEnumSet (Characteristics.UNORDERED); } ציבורי סטטי ImmutableSetCollector toImmutableSet () {להחזיר ImmutableSetCollector חדש (); }

וכאן בפעולה:

רשימה givenList = Arrays.asList ("a", "bb", "ccc", "dddd"); תוצאה ImmutableSet = givenList.stream () .collect (toImmutableSet ());

5. מסקנה

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

כל דוגמאות הקוד זמינות ב- GitHub. תוכלו לקרוא מאמרים מעניינים יותר באתר שלי.