JavaRush /בלוג Java /Random-HE /פופולרי על ביטויי למבדה בג'אווה. עם דוגמאות ומשימות. חלק ...
Стас Пасинков
רָמָה
Киев

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

פורסם בקבוצה
למי מיועד המאמר הזה?
  • למי שקרא את החלק הראשון של מאמר זה;

  • למי שחושב שהוא כבר מכיר היטב את 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(). הערך של המשתנה עצמו אינו משתנה בזמן שהתוכנית פועלת ומצביע תמיד על אותו אובייקט, מה שמאפשר לנו להכריז על משתנה מיד עם מילת המפתח final. אותן דוגמאות, אבל עם ביטויי למבדה:
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("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
    }
}
למרות שהשיטה staticMethod()פרטית, היא "נגישה" להיקרא בתוך השיטה main(), כך שהיא נגישה גם להתקשר מתוך הלמבדה שנוצרת בשיטה main.

רגע הביצוע של קוד ביטוי למבדה

השאלה הזו אולי נראית לכם פשוטה מדי, אבל כדאי לשאול את כל זה: מתי יבוצע הקוד בתוך ביטוי הלמבדה? ברגע הבריאה? או ברגע מתי (עדיין לא ידוע היכן) זה ייקרא? זה די קל לבדוק.
System.out.println("Запуск программы");

// много всякого разного codeа
// ...

System.out.println("Перед объявлением лямбды");

Runnable runnable = () -> System.out.println("Я - лямбда!");

System.out.println("После объявления лямбды");

// много всякого другого codeа
// ...

System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();
פלט בתצוגה:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
ניתן לראות שקוד ביטוי lambda הופעל ממש בסוף, לאחר יצירת השרשור ורק כאשר תהליך הפעלת התוכנית הגיע לביצוע בפועל של השיטה run(). וכלל לא בזמן הכרזתה. על ידי הכרזת ביטוי למבדה, יצרנו רק אובייקט מהסוג Runnableותיארנו את התנהגות השיטה שלו run(). השיטה עצמה הושקה הרבה יותר מאוחר.

הפניות לשיטות?

לא קשור ישירות ללמבדות עצמן, אבל אני חושב שזה יהיה הגיוני לומר על זה כמה מילים במאמר זה. נניח שיש לנו ביטוי למבדה שלא עושה שום דבר מיוחד, אלא רק קורא לשיטה כלשהי.
x -> System.out.println(x)
הם הושיטו לו משהו х, וזה פשוט קרא לו System.out.println()והעביר אותו לשם х. במקרה זה, נוכל להחליף אותו בקישור לשיטה שאנו צריכים. ככה:
System.out::println
כן, בלי הסוגריים בסוף! דוגמה מלאה יותר:
List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");

strings.forEach(x -> System.out.println(x));
בשורה האחרונה אנו משתמשים בשיטה forEach()שמקבלת אובייקט ממשק Consumer. זהו שוב ממשק פונקציונלי עם שיטה אחת בלבד void accept(T t). בהתאם לכך, אנו כותבים ביטוי למבדה שלוקח פרמטר אחד (מכיוון שהוא מוקלד בממשק עצמו, לא מציינים את סוג הפרמטר, אלא מציינים שהוא ייקרא х). בגוף ביטוי הלמבדה נכתוב את הקוד שיבוצע כאשר השיטה תיקרא accept(). כאן אנו פשוט מציגים על המסך מה יש במשתנה х. השיטה עצמה forEach()עוברת על כל האלמנטים של האוסף, קוראת Consumerלמתודה של אובייקט הממשק המועבר אליו (הלמבדה שלנו) accept(), שבו הוא מעביר כל אלמנט מהאוסף. כפי שכבר אמרתי, זהו ביטוי למבדה (פשוט קורא לשיטה אחרת) שאנו יכולים להחליף בהפניה למתודה שאנו צריכים. ואז הקוד שלנו ייראה כך:
List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");

strings.forEach(System.out::println);
העיקר הוא שהפרמטרים המקובלים של השיטות (println()ו accept()). מכיוון שהשיטה println()יכולה לקבל כל דבר (היא עמוסה מדי עבור כל הפרימיטיבים ועבור כל אובייקט), במקום ביטוי למבדה, נוכל להעביר forEach()רק הפניה לשיטה println(). ואז forEach()היא ייקח כל אלמנט באוסף ותעביר אותו ישירות אל השיטה println()למי שנתקל בזה בפעם הראשונה שימו לב שימו לב שאנחנו לא קוראים לשיטה System.out.println()(עם נקודות בין מילים ועם סוגריים בסוף), אלא אנחנו מעבירים את ההתייחסות לשיטה זו עצמה.
strings.forEach(System.out.println());
תהיה לנו שגיאת קומפילציה. מכיוון שלפני הקריאה forEach()ג'אווה תראה שהיא נקראת System.out.println(), היא תבין שהיא מוחזרת voidותנסה voidלהעביר זאת forEach()לאובייקט מסוג שממתין שם Consumer.

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

זה די פשוט:
  1. העברת הפניה לשיטה סטטיתNameКласса:: NameСтатическогоМетода?

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Mother");
            strings.add("soap");
            strings.add("frame");
    
            strings.forEach(Main::staticMethod);
        }
    
        private static void staticMethod(String s) {
            // do something
        }
    }
  2. העברת הפניה לשיטה לא סטטית באמצעות אובייקט קייםNameПеременнойСОбъектом:: method name

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Mother");
            strings.add("soap");
            strings.add("frame");
    
            Main instance = new Main();
            strings.forEach(instance::nonStaticMethod);
        }
    
        private void nonStaticMethod(String s) {
            // do something
        }
    }
  3. אנו מעבירים הפניה למתודה לא סטטית באמצעות המחלקה בה מיושמת מתודה כזוNameКласса:: method name

    public class Main {
        public static void main(String[] args) {
            List<User> users = new LinkedList<>();
            users.add(new User("Vasya"));
            users.add(new User("Коля"));
            users.add(new User("Петя"));
    
            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. העברת קישור לבנאי NameКласса::new
    שימוש בקישורי מתודה נוח מאוד כאשר יש שיטה מוכנה שאתה מרוצה ממנה לחלוטין, ותרצה להשתמש בה כהתקשרות חוזרת. במקרה זה, במקום לכתוב ביטוי למבדה עם הקוד של אותה שיטה, או ביטוי למבדה שבו אנו פשוט קוראים לשיטה הזו, אנו פשוט מעבירים אליו הפניה. זה הכל.

הבדל מעניין בין מעמד אנונימי לביטוי למבדה

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

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

הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION