למי מיועד המאמר הזה?
החומר אינו מתיימר להיות בעל ערך אקדמי כלשהו, שלא לדבר על חידוש. להיפך: אנסה לתאר דברים מורכבים (עבור אנשים מסוימים) בצורה פשוטה ככל האפשר. בקשה להסביר את ה-API של Stream העניקה לי השראה לכתוב את זה. חשבתי על זה והחלטתי שחלק מדוגמאות הזרם שלי יהיו בלתי מובנות ללא הבנה של ביטויי למבדה. אז נתחיל עם ביטויי למבדה.
- זה מיועד לאנשים שקוראים את החלק הראשון של המאמר הזה;
- זה מיועד לאנשים שחושבים שהם כבר מכירים היטב את Java Core, אבל אין להם מושג לגבי ביטויי למבדה בג'אווה. או שאולי שמעו משהו על ביטויי למבדה, אבל הפרטים חסרים.
- זה מיועד לאנשים שיש להם הבנה מסוימת של ביטויי למבדה, אבל עדיין נרתעים מהם ולא רגילים להשתמש בהם.

גישה למשתנים חיצוניים
האם הקוד הזה מתחבר עם מחלקה אנונימית?
int counter = 0;
Runnable r = new Runnable() {
@Override
public void run() {
counter++;
}
};
לא. counter
המשתנה חייב להיות final
. או אם לא final
, אז לפחות זה לא יכול לשנות את הערך שלו. אותו עיקרון חל בביטויי למבדה. הם יכולים לגשת לכל המשתנים שהם יכולים "לראות" מהמקום שהם מוכרזים. אבל למבדה אסור לשנות אותם (להקצות להם ערך חדש). עם זאת, יש דרך לעקוף מגבלה זו בשיעורים אנונימיים. כל שעליך לעשות הוא ליצור משתנה התייחסות ולשנות את המצב הפנימי של האובייקט. בכך, המשתנה עצמו אינו משתנה (מצביע על אותו אובייקט) וניתן לסמן אותו בבטחה כ- final
.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
};
כאן counter
המשתנה שלנו הוא התייחסות לאובייקט AtomicInteger
. והשיטה incrementAndGet()
משמשת לשינוי מצב האובייקט הזה. הערך של המשתנה עצמו אינו משתנה בזמן שהתוכנית פועלת. זה תמיד מצביע על אותו אובייקט, מה שמאפשר לנו להכריז על המשתנה עם מילת המפתח הסופית. הנה אותן דוגמאות, אבל עם ביטויי למבדה:
int counter = 0;
Runnable r = () -> counter++;
זה לא יקומפילציה מאותה סיבה כמו הגרסה עם מחלקה אנונימית: counter
אסור להשתנות בזמן שהתוכנית פועלת. אבל הכל בסדר אם נעשה את זה ככה:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
זה חל גם על שיטות שיחות. בתוך ביטויי למבדה, אתה יכול לא רק לגשת לכל המשתנים ה"גלויים", אלא גם לקרוא לכל שיטות נגישות.
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> staticMethod();
new Thread(runnable).start();
}
private static void staticMethod() {
System.out.println("I'm staticMethod(), and someone just called me!");
}
}
למרות staticMethod()
שהוא פרטי, הוא נגיש בתוך main()
השיטה, כך שניתן לקרוא לו גם מבפנים למבדה שנוצרה בשיטה main
.
מתי מבוצע ביטוי למבדה?
ייתכן שתמצא את השאלה הבאה פשוטה מדי, אבל אתה צריך לשאול אותה בדיוק אותו הדבר: מתי יבוצע הקוד בתוך ביטוי lambda? מתי הוא נוצר? או מתי זה נקרא (שעדיין לא ידוע)? זה די קל לבדוק.
System.out.println("Program start");
// All sorts of code here
// ...
System.out.println("Before lambda declaration");
Runnable runnable = () -> System.out.println("I'm a lambda!");
System.out.println("After lambda declaration");
// All sorts of other code here
// ...
System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start();
פלט מסך:
Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
ניתן לראות שביטוי הלמבדה הופעל ממש בסוף, לאחר יצירת השרשור ורק כאשר הפעלת התוכנית מגיעה לשיטה run()
. בטח לא כשזה מוצהר. על ידי הכרזת ביטוי למבדה, יצרנו רק Runnable
אובייקט ותיארנו כיצד run()
השיטה שלו מתנהגת. השיטה עצמה מבוצעת הרבה יותר מאוחר.
הפניות לשיטות?
הפניות לשיטות אינן קשורות ישירות ללמבדות, אבל אני חושב שזה הגיוני לומר עליהן כמה מילים במאמר זה. נניח שיש לנו ביטוי למבדה שלא עושה שום דבר מיוחד, אלא פשוט קורא לשיטה.
x -> System.out.println(x)
הוא מקבל כמה x
שיחות וסתם System.out.println()
, עובר פנימה x
. במקרה זה, נוכל להחליף אותו בהתייחסות לשיטה הרצויה. ככה:
System.out::println
זה נכון - בלי סוגריים בסוף! הנה דוגמה מלאה יותר:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(x -> System.out.println(x));
בשורה האחרונה אנו משתמשים בשיטה forEach()
שלוקחת אובייקט שמיישם את Consumer
הממשק. שוב, זהו ממשק פונקציונלי, שיש לו רק void accept(T t)
שיטה אחת. בהתאם לכך, אנו כותבים ביטוי למבדה בעל פרמטר אחד (מכיוון שהוא מוקלד בממשק עצמו, איננו מציינים את סוג הפרמטר, רק מציינים שנקרא לו x
). בגוף הביטוי למבדה, אנו כותבים את הקוד שיבוצע כאשר השיטה accept()
תיקרא. כאן אנו פשוט מציגים את מה שהסתיים במשתנה x
. אותה forEach()
שיטה חוזרת על כל האלמנטים באוסף וקוראת לשיטה accept()
על יישום Consumer
הממשק (הלמבדה שלנו), עוברת בכל פריט באוסף. כפי שאמרתי, אנחנו יכולים להחליף ביטוי למבדה כזה (כזה שפשוט מחלק שיטה אחרת) בהתייחסות למתודה הרצויה. אז הקוד שלנו ייראה כך:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(System.out::println);
העיקר שהפרמטרים של השיטות println()
והשיטות accept()
תואמים. מכיוון שהשיטה println()
יכולה לקבל כל דבר (היא עמוסה מדי עבור כל סוגי הפרימיטיבים וכל האובייקטים), במקום ביטויי למבדה, אנחנו יכולים פשוט להעביר הפניה לשיטה println()
ל forEach()
. לאחר מכן forEach()
ייקח כל אלמנט באוסף ויעביר אותו ישירות לשיטה println()
. לכל מי שנתקל בזה בפעם הראשונה, שימו לב שאנחנו לא מתקשרים System.out.println()
(עם נקודות בין מילים ועם סוגריים בסוף). במקום זאת, אנו מעבירים התייחסות לשיטה זו. אם נכתוב את זה
strings.forEach(System.out.println());
תהיה לנו שגיאת קומפילציה. לפני הקריאה אל forEach()
, Java רואה את זה System.out.println()
שנקרא, אז היא מבינה שערך ההחזרה הוא void
וינסה לעבור void
ל- forEach()
, שבמקום זאת מצפה Consumer
לאובייקט.
תחביר להפניות לשיטות
זה די פשוט:-
אנו מעבירים הפניה לשיטה סטטית כמו זו:
ClassName::staticMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); strings.forEach(Main::staticMethod); } private static void staticMethod(String s) { // Do something } }
-
אנו מעבירים הפניה לשיטה לא סטטית באמצעות אובייקט קיים, כך:
objectName::instanceMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); Main instance = new Main(); strings.forEach(instance::nonStaticMethod); } private void nonStaticMethod(String s) { // Do something } }
-
אנו מעבירים הפניה למתודה לא סטטית באמצעות המחלקה המיישמת אותה באופן הבא:
ClassName::methodName
public class Main { public static void main(String[] args) { List<User> users = new LinkedList<>(); users.add (new User("John")); users.add(new User("Paul")); users.add(new User("George")); users.forEach(User::print); } private static class User { private String name; private User(String name) { this.name = name; } private void print() { System.out.println(name); } } }
-
אנו מעבירים הפניה לבנאי כמו זה:
ClassName::new
הפניות לשיטות נוחות מאוד כאשר כבר יש לך שיטה שתעבוד בצורה מושלמת כהתקשרות חוזרת. במקרה זה, במקום לכתוב ביטוי למבדה המכיל את הקוד של השיטה, או לכתוב ביטוי למבדה שפשוט קורא לשיטה, אנו פשוט מעבירים אליו הפניה. וזה הכל.
הבחנה מעניינת בין שיעורים אנונימיים לביטויי למבדה
במחלקה אנונימיתthis
מילת המפתח מצביעה על אובייקט של המחלקה האנונימית. אבל אם נשתמש בזה בתוך למבדה, נקבל גישה לאובייקט של המחלקה המכילה. זה שבו בעצם כתבנו את ביטוי הלמבדה. זה קורה בגלל שביטויי למבדה מורכבים לשיטה פרטית של המחלקה שבה הם נכתבים. לא הייתי ממליץ להשתמש ב"פיצ'ר הזה", מכיוון שיש לה תופעת לוואי וזה סותר את עקרונות התכנות הפונקציונלי. עם זאת, גישה זו תואמת לחלוטין את OOP. ;)
מאיפה קיבלתי את המידע שלי ומה עוד כדאי לקרוא?
- הדרכה באתר הרשמי של אורקל. הרבה מידע מפורט, כולל דוגמאות.
- פרק על הפניות לשיטות באותו מדריך של אורקל.
- תישאב לויקיפדיה אם אתה באמת סקרן.
GO TO FULL VERSION