ההבדל בין Collection.stream (). ForEach () ו- Collection.forEach ()

1. הקדמה

ישנן מספר אפשרויות לחזור על אוסף ב- Java. במדריך קצר זה נבחן שתי גישות דומות למראה - Collection.stream (). ForEach () ו Collection.forEach ().

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

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

ראשית, בואו ליצור רשימה לאיתור:

רשימת רשימה = Arrays.asList ("A", "B", "C", "D");

הדרך הכי פשוטה היא להשתמש ב- for-loop המשופר:

עבור (String s: list) {// לעשות משהו עם s} 

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

הצרכן הצרכן = s -> {System.out :: println}; list.forEach (צרכן); 

או שאנחנו יכולים להתקשר לכל אחד() בזרם האוסף:

list.stream (). forEach (צרכן); 

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

ABCD ABCD

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

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

Collection.forEach () משתמש באיטרטור האוסף (אם מצוין אחד כזה). כלומר, מוגדר סדר העיבוד של הפריטים. לעומת זאת, צו העיבוד של Collection.stream (). ForEach () אינו מוגדר.

ברוב המקרים, אין זה משנה איזה מבין השניים אנו בוחרים.

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

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

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

list.forEach (System.out :: print); System.out.print (""); list.parallelStream (). forEach (System.out :: print); 

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

פלט אפשרי אחד הוא:

ABCD CDBA

אחד נוסף הוא:

ABCD DBCA

3.2. מחזיקים מותאמים אישית

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

class ReverseList מרחיב את ArrayList {@Override Iterator public Iterator () {int startIndex = this.size () - 1; רשימת רשימה = זה; Iterator it = Iterator חדש () {private int currentIndex = startIndex; @Override בוליאני ציבורי hasNext () {return currentIndex> = 0; } @ עקירה ציבורית מחרוזת הבאה () {מחרוזת הבאה = list.get (currentIndex); currentIndex--; חזור הבא; } @ ביטול בטל ציבורי להסיר () {לזרוק חדש UnsupportedOperationException (); }}; החזר את זה; }} 

כשאנחנו חוזרים על הרשימה, שוב עם לכל אחד() ישירות באוסף ואז בזרם:

רשימת myList = ReverseList חדש (); myList.addAll (רשימה); myList.forEach (System.out :: print); System.out.print (""); myList.stream (). forEach (System.out :: print); 

אנו מקבלים תוצאות שונות:

DCBA ABCD 

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

4. שינוי האוסף

אוספים רבים (למשל, רשימת מערך אוֹ HashSet) לא צריך לשנות מבנה תוך כדי איטרציה מעליהם. אם אלמנט יוסר או יתווסף במהלך איטרציה, נקבל ConcurrentModification יוצא מן הכלל.

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

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

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

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

4.1. הסרת אלמנט

בואו נגדיר פעולה שמסירה את האלמנט האחרון ("D") ברשימה שלנו:

הצרכן removeElement = s -> {System.out.println (s + "" + list.size ()); אם (s! = null && s.equals ("A")) {list.remove ("D"); }};

כאשר אנו חוזרים על הרשימה, האלמנט האחרון מוסר לאחר הדפסת האלמנט הראשון ("A"):

list.forEach (removeElement);

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

חריג 4 בחוט "הראשי" java.util.ConcurrentModificationException ב java.util.ArrayList.forEach (ArrayList.java:1252) ב- ReverseList.main (ReverseList.java:1)

בואו נראה מה יקרה אם נשתמש זרם (). forEach () במקום:

list.stream (). forEach (removeElement);

כאן אנו ממשיכים לחזור על כל הרשימה לפני שנראה חריג:

A 4 B 3 C 3 null 3 חריג בשרשור "ראשי" java.util.ConcurrentModificationException ב- java.util.ArrayList $ ArrayListSpliterator.forEachRemaining (ArrayList.java:1380) בכתובת java.util.stream.ReferencePipeline $ Head.forEach (ReferencePipeline .java: 580) ב- ReverseList.main (ReverseList.java:1)

עם זאת, ג'אווה אינה מתחייבת כי א ConcurrentModificationException נזרק בכלל. כלומר לעולם לא נצטרך לכתוב תוכנית שתלויה בחריג זה.

4.2. אלמנטים משתנים

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

list.forEach (e -> {list.set (3, "E");});

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

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

5. מסקנה

במאמר זה ראינו כמה דוגמאות המראות את ההבדלים הדקים בין Collection.forEach () ו Collection.stream (). ForEach ().

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

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

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


$config[zx-auto] not found$config[zx-overlay] not found