צינורות במקום לולאות
Stream הוא צינור לעיבוד רצף של ערכים. מתחילים ממקור, בדרך כלל אוסף, משרשרים פעולות כמו "השאר רק את אלה", "המר כל אחד", "מיין אותם", ומסיימים באיסוף התוצאה. במקום לכתוב לולאה עם רשימה זמנית ו-if בפנים, מתארים מה רוצים ונותנים ל-stream לעשות את המעבר.
עכשיו כשיש לכם lambdas, streams נופלים למקום: כל פעולה מקבלת lambda (או method reference) שמתארת את העבודה עבור איבר אחד.
קראו מלמעלה למטה: קחו את השמות, השאירו את הארוכים, הפכו לאותיות גדולות, אספו לרשימה. הרשימה המקורית names לא משתנה אף פעם: stream מפיק תוצאה חדשה ומשאיר את המקור בשקט.
האנטומיה של stream
לכל צינור יש שלושה חלקים:
- מקור:
list.stream(),Arrays.stream(array)אוStream.of(a, b, c). - אפס או יותר פעולות ביניים:
filter,map,sorted,distinct,limit. כל אחת מחזירה stream נוסף, ולכן אפשר לשרשר אותן. - בדיוק פעולה סופית אחת:
collect,count,forEach,reduce,findFirst. היא מתניעה את העבודה ומפיקה תוצאה (או תופעת לוואי).
החלוקה הזו חשובה בגלל כלל אחד שמפתיע את כולם בהתחלה.
פעולות ביניים הן עצלות
פעולות ביניים לא עושות כלום בעצמן. הן רק רושמות מה צריך לקרות. הצינור רץ רק כשפעולה סופית מבקשת תוצאה.
הריצו את זה: ה-lambda של map לא רצה אף פעם. הוסיפו פעולה סופית, וכל השרשרת מתעוררת לחיים:
העצלות הזו היא לא תכונה מוזרה שצריך להילחם בה: היא מאפשרת ל-stream למזג פעולות ולדלג על עבודה מיותרת. אבל המשמעות היא שצינור בלי פעולה סופית לא עושה כלום, וזה באג נפוץ מסוג "למה שום דבר לא קורה?".
filter ו-map: לשמור ולהמיר
שתי אלה נושאות את רוב המשקל. filter מקבלת Predicate (lambda שמחזירה boolean) ושומרת את האיברים שעוברים. map מקבלת Function ומחליפה כל איבר בתוצאה של הפעלתה עליו.
שימו לב ש-map יכולה לשנות את טיפוס האיבר: כאן stream של String הופך ל-stream של Integer. String::length הוא method reference, קיצור של ה-lambda w -> w.length().
פעולות סופיות מפיקות תוצאה
אחרי שעיצבתם את ה-stream, פעולה סופית הופכת אותו למשהו מוחשי:
פעולות סופיות נפוצות: collect (איסוף לרשימה, קבוצה או מפה), count, forEach, anyMatch / allMatch / noneMatch, findFirst, min / max ו-reduce. אחרי שפעולה סופית רצה, ה-stream נצרך: אי אפשר להשתמש בו שוב. קראו שוב ל-list.stream() כדי לקבל צינור חדש.
איסוף תוצאות
collect עם Collectors הוא סוס העבודה לבניית תוצאה. הנפוץ ביותר הוא Collectors.toList():
Collectors נותנת גם את toSet(), joining(...), groupingBy(...) ו-counting(). ב-Java 16 ומעלה אפשר להחליף את collect(Collectors.toList()) ב-.toList() הקצר יותר (הוא מחזיר רשימה שאי אפשר לשנות):
List<String> result = names.stream().map(String::toUpperCase).toList();
sorted, distinct ו-limit
פעולות הביניים האלה מעצבות מחדש את ה-stream לפני האיסוף:
sorted() בלי ארגומנט משתמשת בסדר הטבעי; העבירו Comparator (כאן lambda) לסדר מותאם. Comparator.reverseOrder() היא הדרך הנקייה יותר למיין בסדר יורד.
reduce: לקפל stream לערך אחד
כשצריך לשלב את כל האיברים לתוצאה אחת, סכום, מכפלה, המחרוזת הארוכה ביותר, reduce היא הכלי הכללי. נותנים לה ערך התחלתי ופונקציה שממזגת שני ערכים:
לסכומים וממוצעים פשוטים, ה-streams הייעודיים ברורים יותר: nums.stream().mapToInt(Integer::intValue).sum(). השתמשו ב-reduce כשאין פונקציית צבירה מוכנה.
Streams לא מחליפים כל לולאה
Streams מצטיינים כשממירים אוסף לתוצאה. הם לא מהירים יותר מלולאה באופן אוטומטי, והם מסורבלים כשצריך לשנות מצב חיצוני או לצאת מוקדם בדרכים מסובכות. כלל טוב: אם הצינור נקרא כמשפט אחד ברור, השתמשו ב-stream; אם אתם מושיטים יד למונה משותף או ל-index, לולאת for רגילה היא כנה ובסדר גמור.
זכרו גם ש-stream הוא לשימוש חד פעמי. פעולה עליו פעמיים זורקת חריגה:
Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count(); // IllegalStateException: stream has already been operated upon
הבא: Optional
כמה פעולות סופיות של stream, findFirst, min, max, ו-reduce בלי ערך התחלתי, עשויות לא למצוא כלום, ולכן הן לא מחזירות ערך חשוף. הן מחזירות Optional, המיכל של Java ל"אולי ערך, אולי כלום", שסוף סוף נותן חלופה נקייה להחזרת null. זה העמוד הבא.
שאלות נפוצות
מה זה stream ב-Java?
Stream הוא צינור לעיבוד רצף של איברים, בדרך כלל ממקור של אוסף, דרך שרשרת פעולות כמו filter, map ו-sorted, שמסתיימת בפעולה סופית כמו collect או count. הוא לא שומר נתונים ולא משנה את המקור; הוא מתאר חישוב. בונים אותו עם list.stream() וקוראים את השרשרת מלמעלה למטה, כמו מתכון.
איך ממירים stream בחזרה לרשימה ב-Java?
מסיימים את הצינור עם collector סופי: list.stream().filter(...).collect(Collectors.toList()). ב-Java 16 ומעלה אפשר להשתמש ב-.toList() הקצר יותר, שמחזיר רשימה שאי אפשר לשנות. בלי פעולה סופית שום דבר לא רץ, כי פעולות ביניים כמו filter ו-map הן עצלות.
מתי כדאי להשתמש ב-stream במקום בלולאת for?
השתמשו ב-stream כשממירים או מסננים אוסף לתוצאה: הצינור נקרא כמשפט אחד ברור ("קחו את השמות, השאירו את הארוכים, הפכו לאותיות גדולות, אספו לרשימה"). הישארו עם לולאת for רגילה כשצריך לשנות מצב חיצוני, לצאת מוקדם בדרכים מורכבות, או כשהלוגיקה פשוטה יותר כצעדים אימפרטיביים. Streams עוסקים בבהירות, לא במהירות גולמית.