Menu

מעבר על Collections ב-Java: for-each, Iterator, forEach

הדרכים לעבור על אוספים ב-Java: לולאת for-each, ה-Iterator, לולאות עם אינדקס והמתודה forEach, ואיך למחוק איברים בבטחה תוך כדי מעבר.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

משימה אחת, כמה כלים

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

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

קראו את הנקודתיים כ"בתוך": "לכל lang בתוך langs". אתם לא נוגעים באינדקס בכלל, אז אין פה מה לפספס.

לולאת for-each

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

דבר אחד שכדאי לזכור לגבי HashSet: אין לו סדר מוגדר, אז האיברים יכולים להודפס בכל סדר שהוא. לולאת for-each עוברת על כולם בדיוק פעם אחת בכל מקרה.

כשצריך את האינדקס

לולאת for-each נותנת את האיבר אבל לא את המיקום שלו. אם באמת צריך את האינדקס, כדי למספר שורות או להסתכל על איברים שכנים, השתמשו בלולאה עם מונה, size() ו-get(i). זה עובד על List, שמבוסס על מיקומים. לקבוצות ולמפות אין אינדקס, אז הסגנון הזה לא מתאים להן.

אל תבחרו בזה רק מתוך הרגל. אם אתם לא משתמשים ב-i לשום דבר מעבר ל-get(i), גרסת ה-for-each קצרה יותר וקשה יותר לטעות בה.

מעבר על Map

Map הוא לא Collection, ולכן אי אפשר להפעיל עליו for-each ישירות. במקום זה עוברים על אחת משלוש התצוגות שלו. הנפוצה ביותר היא entrySet(), שמגישה כל זוג מפתח וערך יחד:

אם צריך רק את המפתחות, עברו על ages.keySet(). אם צריך רק את הערכים, עברו על ages.values(). כשצריך את שניהם העדיפו את entrySet(): מעבר על המפתחות וקריאה ל-ages.get(key) בתוך הלולאה מבצע חיפוש נוסף בכל איטרציה בלי שום סיבה.

ה-Iterator

לולאת for-each היא בעצם קיצור תחבירי מעל Iterator: אובייקט שעובר על אוסף איבר אחרי איבר בעזרת hasNext() ו-next(). כמעט אף פעם לא כותבים את הלולאה הזו ידנית, עם חריג חשוב אחד: זו הדרך הבטוחה למחוק איברים תוך כדי מעבר.

it.remove() מוחק את האיבר ש-next() החזיר לאחרונה, וה-iterator נשאר תקין. זו הדרך המאושרת היחידה לשנות אוסף במהלך לולאה ידנית.

המלכודת: ConcurrentModificationException

אם קוראים ל-add או ל-remove על האוסף עצמו בתוך לולאת for-each, מקבלים ConcurrentModificationException: ה-iterator שם לב שהאוסף השתנה מתחתיו ומסרב להמשיך. זו אחת השגיאות הנפוצות ביותר אצל מתחילים, וכמו כל חריגת זמן ריצה היא עוצרת את המתודה, אלא אם נמנעים מלגרום לה מלכתחילה.

List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4));
for (int n : nums) {
    if (n % 2 == 0) {
        nums.remove(Integer.valueOf(n));   // throws ConcurrentModificationException
    }
}

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

removeIf עובד על כל Collection, כך שאותה קריאה מנקה גם HashSet.

המתודה forEach

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

שימו לב ש-Map.forEach מקבלת ישירות lambda עם שני ארגומנטים (key, value), בלי צורך ב-entrySet(). השתמשו ב-forEach לפעולות צד מהירות, וחזרו ללולאת for הרגילה כשהגוף גדל או כשצריך לצאת מוקדם עם break, דבר ש-lambda לא יכולה לעשות.

הבא בתור: מתודות

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

שאלות נפוצות

איך עוברים על רשימה ב-Java?

הדרך הנקייה ביותר היא לולאת for המשופרת (for-each): for (String s : list) { ... }. היא עובדת על כל Collection: ArrayList, HashSet וכן הלאה. השתמשו בלולאה עם אינדקס ו-get(i) רק כשבאמת צריך את המיקום, וב-Iterator כשצריך למחוק איברים במהלך הלולאה.

איך עוברים על HashMap ב-Java?

Map הוא לא Collection, ולכן עוברים על אחת התצוגות שלו. הבחירה הרגילה היא for (Map.Entry<K, V> e : map.entrySet()), שנותנת גם את המפתח (e.getKey()) וגם את הערך (e.getValue()) במעבר אחד. אפשר גם לעבור על map.keySet() בשביל המפתחות או על map.values() בשביל הערכים.

למה אני מקבל ConcurrentModificationException בזמן לולאה?

קראתם ל-add או ל-remove על האוסף בזמן שלולאת for-each עברה עליו. לולאת for-each משתמשת מאחורי הקלעים ב-Iterator, והוא מזהה שהאוסף השתנה מבנית. הפתרון הוא למחוק דרך המתודה remove() של ה-Iterator עצמו, או לקרוא ל-removeIf(...) במקום לולאה.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל