התנהגות שאפשר להעביר הלאה
ביטוי lambda הוא גוש קוד קומפקטי שאפשר למסור למתודה, לשמור במשתנה או להחזיר, בדיוק כמו מספר או מחרוזת. לפני שהיו ביטויי lambda, העברת התנהגות חייבה לכתוב מחלקה שלמה (או מחלקה אנונימית מסורבלת) רק כדי לעטוף מתודה אחת. lambda מזקקת את זה לעיקר: הפרמטרים והגוף.
lambda תמיד מממשת ממשק פונקציונלי: ממשק עם מתודה מופשטת אחת בדיוק (פגשתם אותם בסוף העמוד על ממשקים). הקומפיילר מבין מההקשר לאיזה ממשק התכוונתם ומתאים את ה-lambda למתודה היחידה הזו.
x -> x * 2 הוא כל המימוש של apply. אין new, אין גוף מחלקה, אין שם מתודה: הממשק מספק את כל אלה.
תחביר החץ
לכל lambda יש את הצורה parameters -> body. החלקים משתנים לפי מה שצריך:
() -> 42 // בלי פרמטרים
x -> x + 1 // פרמטר אחד, הסוגריים אופציונליים
(x, y) -> x + y // שניים או יותר פרמטרים צריכים סוגריים
(int x, int y) -> x + y // הטיפוסים אופציונליים: בדרך כלל הם מוסקים
x -> { // גוף של בלוק צריך סוגריים מסולסלים ו-return
int doubled = x * 2;
return doubled + 1;
}
גוף שהוא ביטוי יחיד (x -> x + 1) מחזיר את הערך שלו באופן מובלע, בלי מילת המפתח return. ברגע שמשתמשים בסוגריים מסולסלים, כותבים בלוק רגיל וחייבים לכתוב return במפורש אם המתודה מחזירה משהו. טעות נפוצה היא לערבב בין השניים: x -> { x + 1 } לא מתקמפל, כי בלוק צריך פקודה (return x + 1;).
lambda במקום מחלקות אנונימיות
הדרך הברורה ביותר לראות מה lambda נותנת היא לפני ואחרי. מיון עם Comparator מותאם אישית נראה פעם ככה:
// לפני: מחלקה אנונימית
names.sort(new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
אותו דבר כ-lambda הוא שורה אחת קריאה:
Comparator הוא ממשק פונקציונלי (המתודה המופשטת היחידה שלו היא compare), אז ה-lambda נכנסת ישר למקום. כל הטקס, ה-new, המחלקה, חתימת המתודה, נעלם, ונשארת רק לוגיקת ההשוואה.
ארגז הכלים של java.util.function
כמעט אף פעם לא צריך להצהיר על ממשק פונקציונלי משלכם. החבילה java.util.function מגיעה עם הצורות הנפוצות, וכמעט כל ממשקי הספרייה של Java מקבלים אותן:
Function<T, R>: מקבלתT, מחזירהR(apply)Predicate<T>: מקבלתT, מחזירהboolean(test)Supplier<T>: לא מקבלת כלום, מחזירהT(get)Consumer<T>: מקבלתT, לא מחזירה כלום (accept)
אלה ממשקים גנריים: Function<String, Integer> משתמש שוב ב-generics שראיתם בעמוד הקודם כדי לשמור על בטיחות טיפוסים. בחרו את הממשק שהצורה שלו מתאימה למה שהקוד שלכם צריך לקבל ולהפיק.
הפניות למתודות
כש-lambda לא עושה כלום חוץ מלקרוא למתודה קיימת אחת, אפשר להחליף אותה בהפניה למתודה עם ::. זה אותו ערך, כתוב בצורה ישירה יותר:
להפניות למתודות יש כמה סוגים: String::toUpperCase (מתודת מופע שנקראת על כל ארגומנט), Math::max (מתודה סטטית), System.out::println (מתודה על אובייקט מסוים) ו-ArrayList::new (בנאי). השתמשו בהן רק כשהן נקראות בבירור: אם צריך לחשוב איזו צורה מתאימה, lambda רגילה זה בסדר גמור.
לכידת משתנים
lambda יכולה להשתמש במשתנים מקומיים מהמתודה שסביבה, אבל רק אם הם final או effectively final: הושמו פעם אחת ולא השתנו לעולם. Java לוכדת את הערך ברגע שה-lambda נוצרת, ולכן משתנה שעלול להשתנות אחר כך היה דו-משמעי.
אם תשימו ערך חדש ב-factor במקום כלשהו, הקומפיילר ידחה את ה-lambda עם "variable used in lambda expression should be final or effectively final". כשבאמת צריך מצב משותף שמשתנה, לכדו אובייקט במקום: שדה, איבר של מערך או AtomicInteger, כי ההפניה נשארת קבועה גם כשהתוכן שלה משתנה. שימו לב שביטויי lambda, בניגוד למחלקות אנונימיות, לא יוצרים scope משלהם: this בתוך lambda מתייחס למופע העוטף, לא ל-lambda עצמה.
הבא בתור: Streams
ביטויי lambda הם אבן הבניין, לא היעד. התמורה האמיתית שלהם מגיעה עם ה-Streams API, שבו משרשרים פעולות כמו filter, map ו-reduce, שכל אחת מהן מקבלת lambda, כדי לבטא טרנספורמציות של נתונים כצינור קריא במקום סבך של לולאות. זה העמוד הבא.
שאלות נפוצות
מה זה ביטוי lambda ב-Java?
lambda היא דרך קצרה לכתוב מופע של ממשק פונקציונלי: ממשק עם מתודה מופשטת אחת בלבד. במקום מחלקה אנונימית שלמה כותבים parameters -> body. הקומפיילר מתאים את ה-lambda למתודה היחידה של הממשק, כך ש-Runnable r = () -> System.out.println("hi"); הוא Runnable שלם. ביטויי lambda מאפשרים להעביר התנהגות ממקום למקום כמו נתונים.
מה ההבדל בין lambda להפניה למתודה ב-Java?
שתיהן יוצרות מופע של ממשק פונקציונלי. ל-lambda יש פרמטרים מפורשים וגוף (s -> s.toUpperCase()), ואילו הפניה למתודה היא קיצור של lambda שרק קוראת למתודה קיימת אחת (String::toUpperCase). השתמשו בהפניה למתודה כשה-lambda לא הייתה עושה שום דבר חוץ מלהעביר את הארגומנטים שלה למתודה אחת עם שם: זה קצר יותר ונקרא טוב יותר.
למה משתנים שמשמשים בתוך lambda ב-Java חייבים להיות final או effectively final?
lambda יכולה ללכוד משתנים מקומיים מהמתודה שעוטפת אותה, אבל רק אם הם לא משתנים אף פעם אחרי ההשמה. זו המשמעות של "effectively final". Java לוכדת את הערך, לא הפניה חיה למשתנה, ולכן לאפשר השמה מחדש היה דו-משמעי ולא בטוח בין תהליכונים. אם צריך מצב משותף שמשתנה, השתמשו בשדה או בעטיפה כמו מערך או AtomicInteger.