מדריך Stream.reduce ()

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

ממשק ה- API של Stream מספק רפרטואר עשיר של פונקציות ביניים, צמצום וטרמינל, שתומכות גם בהקבלה.

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

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

2. מושגי המפתח: זהות, צבר ומשלב

לפני שנבדוק לעומק את השימוש ב- Stream.reduce () פעולה, בואו נפרק את מרכיבי המשתתפים בפעולה לבלוקים נפרדים. בדרך זו נבין ביתר קלות את התפקיד שממלא כל אחד:

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

3. שימוש Stream.reduce ()

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

מספרי רשימה = Arrays.asList (1, 2, 3, 4, 5, 6); int result = numbers .stream () .reduce (0, (subtotal, element) -> subtotal + יסוד); assertThat (תוצאה). isEqualTo (21);

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

כְּמוֹ כֵן, הביטוי למבדה:

סכום המשנה, אלמנט -> סכום המשנה + אלמנט

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

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

int result = numbers.stream (). להפחית (0, Integer :: sum); assertThat (תוצאה). isEqualTo (21);

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

למשל, אנו יכולים להשתמש לְהַפחִית() על מערך של חוּט אלמנטים והצטרף אליהם לתוצאה אחת:

אותיות רשימה = Arrays.asList ("a", "b", "c", "d", "e"); תוצאת מחרוזת = אותיות. Stream () .reduce ("", (partialString, element) -> partialString + element); assertThat (תוצאה) .isEqualTo ("abcde");

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

תוצאת מחרוזת = letters.stream (). Reduce ("", String :: concat); assertThat (תוצאה) .isEqualTo ("abcde");

בואו נשתמש ב- לְהַפחִית() פעולה להצטרפות לאלמנטים הגדולים של אותיות מַעֲרָך:

תוצאת מחרוזת = אותיות .stream () .reduce ("", (partialString, element) -> partialString.toUpperCase () + element.toUpperCase ()); assertThat (תוצאה) .isEqualTo ("ABCDE");

בנוסף, אנו יכולים להשתמש לְהַפחִית() בזרם מקביל (על כך בהמשך):

רשימת גילאים = Arrays.asList (25, 30, 45, 28, 32); int computedAges = age.parallelStream (). להפחית (0, a, b -> a + b, שלם :: סכום);

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

מספיק מצחיק, קוד זה לא יתכנס:

משתמשים ברשימה = Arrays.asList (משתמש חדש ("ג'ון", 30), משתמש חדש ("ג'ולי", 35)); int computedAges = users.stream (). להפחית (0, (partialAgeResult, משתמש) -> partialAgeResult + user.getAge ()); 

במקרה זה, יש לנו זרם של מִשׁתַמֵשׁ אובייקטים וסוגי טיעוני הצבירה הם מספר שלם ו מִשׁתַמֵשׁ. עם זאת, יישום הצבר הוא סכום של שלמים, כך שהמהדר פשוט לא יכול להסיק את סוג ה- מִשׁתַמֵשׁ פָּרָמֶטֶר.

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

int result = users.stream () .reduce (0, (partialAgeResult, user) -> partialAgeResult + user.getAge (), Integer :: sum); assertThat (תוצאה). isEqualTo (65);

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

4. צמצום במקביל

כפי שלמדנו קודם, אנו יכולים להשתמש לְהַפחִית() על זרמים מקבילים.

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

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

עלינו למלא את כל התנאים הללו כדי למנוע תוצאות בלתי צפויות.

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

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

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

בואו ליצור מבחן ביצועים פשוט של JMH (Java Microbenchmark Harness) ונשווה את זמני הביצוע בהתאמה בעת השימוש ב- לְהַפחִית() פעולה על זרם רציף ומקביל:

@State (Scope.Thread) רשימה סופית פרטית userList = createUsers (); @Benchmark שלם ציבורי executeReduceOnParallelizedStream () {להחזיר this.userList .parallelStream () .reduce (0, (partialAgeResult, משתמש) -> partialAgeResult + user.getAge (), שלם :: סכום); } @Benchmark ציבורי שלם executeReduceOnSequentialStream () {להחזיר this.userList .stream () .reduce (0, (partialAgeResult, משתמש) -> partialAgeResult + user.getAge (), שלם :: סכום); } 

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

אלו התוצאות המדהימות שלנו:

יחידות מידה של מידוד Cnt ציון שגיאות

5. השלכה וטיפול בחריגים תוך צמצום

בדוגמאות לעיל, לְהַפחִית() הפעולה אינה מביאה חריגים. אבל זה עשוי, כמובן.

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

מספרי רשימה = Arrays.asList (1, 2, 3, 4, 5, 6); מחלק int = 2; int result = numbers.stream (). להפחית (0, a / divider + b / divider); 

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

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

ציבורי סטטי ציבורי int divideListElements (ערכי רשימה, מחלק int) {return values.stream () .reduce (0, (a, b) -> {try {return a / divider + b / divider;} catch (ArithmeticException e) {LOGGER .log (Level.INFO, "חריג אריתמטי: חלוקה לפי אפס");} להחזיר 0;}); }

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

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

חלוקה אינטטית פרטית (int ערך, int factor) {int result = 0; נסה {result = value / factor; } לתפוס (ArithmeticException e) {LOGGER.log (Level.INFO, "חריג אריתמטי: חלוקה לפי אפס"); } החזר תוצאה} 

כעת, יישום ה- divideListElements () השיטה נקייה ויעילה:

ציבורי סטטי ציבורי int divideListElements (ערכי רשימה, int מחלק) {return values.stream (). להפחית (0, (a, b) -> לחלק (a, לחלק) + לחלק (b, לחלק)); } 

בהנחה ש divideListElements () היא שיטת שימוש המיושמת על ידי תקציר NumberUtils בכיתה, אנו יכולים ליצור מבחן יחידה לבדיקת התנהגות ה- divideListElements () שיטה:

מספרי רשימה = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (מספרים, 1)). isEqualTo (21); 

בואו גם לבדוק את divideListElements () השיטה, כאשר המסופקת רשימה שֶׁל מספר שלם ערכים מכילים 0:

מספרי רשימה = Arrays.asList (0, 1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (מספרים, 1)). isEqualTo (21); 

לבסוף, בואו לבדוק את יישום השיטה גם כאשר המחלק הוא 0:

מספרי רשימה = Arrays.asList (1, 2, 3, 4, 5, 6); assertThat (NumberUtils.divideListElements (מספרים, 0)). isEqualTo (0);

6. אובייקטים מורכבים בהתאמה אישית

אָנוּ יכול גם להשתמש Stream.reduce () עם אובייקטים מותאמים אישית המכילים שדות שאינם פרימיטיביים. לשם כך, עלינו לספק iשיניים, מצבר, ו קומבינר לסוג הנתונים.

נניח שלנו מִשׁתַמֵשׁ הוא חלק מאתר ביקורות. כל אחד משלנו מִשׁתַמֵשׁזה יכול להחזיק אחד דֵרוּג, הממוצע על פני רבים סקירהס.

ראשית, נתחיל עם שלנו סקירה לְהִתְנַגֵד. כל אחד סקירה צריך להכיל הערה וציון פשוטים:

סקירה בכיתה ציבורית {נקודות אינטימיות פרטיות; סקירת מחרוזות פרטית; // קונסטרוקטור, גטרים וקובעים}

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

דירוג בכיתה ציבורית {נקודות כפולות; ביקורות רשימות = ArrayList חדש (); הוסף חלל ציבורי (סקירת ביקורת) {reviews.add (סקירה); computeRating (); } כפול פרטי computeRating () {double totalPoints = ביקורות.זרם (). מפה (סקירה :: getPoints) .פחת (0, שלם :: סכום); this.points = totalPoints / reviews.size (); להחזיר את זה. נקודות; } ממוצע דירוג סטטי ציבורי (דירוג r1, דירוג r2) {דירוג משולב = דירוג חדש (); combined.reviews = ArrayList חדש (r1.reviews); combined.reviews.addAll (r2.reviews); combined.computeRating (); החזר משולב; }}

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

לאחר מכן, בואו נגדיר רשימה של מִשׁתַמֵשׁs, כל אחד עם סט ביקורות משלו.

משתמש ג'ון = משתמש חדש ("ג'ון", 30); john.getRating (). הוסף (סקירה חדשה (5, "")); john.getRating (). הוסף (סקירה חדשה (3, "לא רע")); משתמש ג'ולי = משתמש חדש ("ג'ולי", 35); john.getRating (). הוסף (סקירה חדשה (4, "נהדר!")); john.getRating (). הוסף (סקירה חדשה (2, "חוויה איומה")); john.getRating (). הוסף (סקירה חדשה (4, "")); משתמשים ברשימה = Arrays.asList (ג'ון, ג'ולי); 

עכשיו, כאשר ג'ון וג'ולי מתחשבים, בואו נשתמש Stream.reduce () כדי לחשב דירוג ממוצע בין שני המשתמשים. בתור זהותבוא נחזיר חדש דֵרוּג אם רשימת הקלטים שלנו ריקה:

דירוג ממוצע דירוג = משתמשים.זרם () .פחת (דירוג חדש (), (דירוג, משתמש) -> דירוג.ממוצע (דירוג, user.getRating ()), דירוג :: ממוצע);

אם אנו עושים את המתמטיקה, עלינו לגלות שהציון הממוצע הוא 3.6:

assertThat (averageRating.getPoints ()). isEqualTo (3.6);

7. מסקנה

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

כרגיל, כל דגימות הקוד המוצגות במדריך זה זמינות ב- GitHub.