CodeGym /בלוג Java /Random-HE /הסבר על ביטויי למבדה בג'אווה. עם דוגמאות ומשימות. חלק 2
John Squirrels
רָמָה
San Francisco

הסבר על ביטויי למבדה בג'אווה. עם דוגמאות ומשימות. חלק 2

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

גישה למשתנים חיצוניים

האם הקוד הזה מתחבר עם מחלקה אנונימית?

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לאובייקט.

תחביר להפניות לשיטות

זה די פשוט:
  1. אנו מעבירים הפניה לשיטה סטטית כמו זו: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 
        } 
    }
  2. אנו מעבירים הפניה לשיטה לא סטטית באמצעות אובייקט קיים, כך: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 
        } 
    }
  3. אנו מעבירים הפניה למתודה לא סטטית באמצעות המחלקה המיישמת אותה באופן הבא: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); 
            } 
        } 
    }
  4. אנו מעבירים הפניה לבנאי כמו זה:ClassName::new

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

הבחנה מעניינת בין שיעורים אנונימיים לביטויי למבדה

במחלקה אנונימית thisמילת המפתח מצביעה על אובייקט של המחלקה האנונימית. אבל אם נשתמש בזה בתוך למבדה, נקבל גישה לאובייקט של המחלקה המכילה. זה שבו בעצם כתבנו את ביטוי הלמבדה. זה קורה בגלל שביטויי למבדה מורכבים לשיטה פרטית של המחלקה שבה הם נכתבים. לא הייתי ממליץ להשתמש ב"פיצ'ר הזה", מכיוון שיש לה תופעת לוואי וזה סותר את עקרונות התכנות הפונקציונלי. עם זאת, גישה זו תואמת לחלוטין את OOP. ;)

מאיפה קיבלתי את המידע שלי ומה עוד כדאי לקרוא?

וכמובן, מצאתי המון דברים בגוגל :)
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION