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

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

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

קצת היסטוריה

ביטויי למדה הגיעו לג'אווה מהתכנות הפונקציונלי, ושם מהמתמטיקה. באמצע המאה ה-20 באמריקה עבד באוניברסיטת פרינסטון כנסיית אלונזו מסוימת, שאהב מאוד מתמטיקה וכל מיני הפשטות. אלונזו צ'רץ' היה זה שהגה את חשבון למבדה, שבהתחלה היה אוסף של כמה רעיונות מופשטים ולא היה לו שום קשר לתכנות. במקביל, מתמטיקאים כמו אלן טיורינג וג'ון פון נוימן עבדו באותה אוניברסיטת פרינסטון. הכל התאחד: צ'רץ' המציא את מערכת חישוב למבדה, טיורינג פיתח את מכונת המחשוב המופשטת שלו, הידועה כיום בשם "מכונת טיורינג". ובכן, פון נוימן הציע דיאגרמה של ארכיטקטורת המחשבים, שהיווה את הבסיס של המחשבים המודרניים (ומכונה כיום "ארכיטקטורת פון נוימן"). באותה תקופה, רעיונותיו של אלונזו צ'רץ' לא זכו לתהילה רבה כמו עבודתם של עמיתיו (למעט תחום המתמטיקה ה"טהורה"). אולם, קצת מאוחר יותר, ג'ון מקארתי מסוים (גם הוא בוגר אוניברסיטת פרינסטון, בזמן הסיפור - עובד המכון הטכנולוגי של מסצ'וסטס) התעניין ברעיונותיו של צ'רץ'. בהתבסס עליהם, ב-1958 הוא יצר את שפת התכנות הפונקציונלית הראשונה, Lisp. ו-58 שנים מאוחר יותר, הרעיונות של תכנות פונקציונלי דלפו לג'אווה כמספר 8. אפילו 70 שנה לא עברו... למעשה, זה לא פרק הזמן הארוך ביותר ליישום רעיון מתמטי בפועל.

המהות

ביטוי למבדה הוא פונקציה כזו. אתה יכול לחשוב על זה כעל שיטה רגילה ב-Java, ההבדל היחיד הוא שניתן להעביר אותה לשיטות אחרות בתור ארגומנט. כן, אפשר היה להעביר לא רק מספרים, מחרוזות וחתולים לשיטות, אלא גם שיטות אחרות! מתי אולי נצטרך את זה? לדוגמה, אם אנחנו רוצים להעביר קצת התקשרות חוזרת. אנחנו צריכים את השיטה שאנחנו קוראים לה כדי שנוכל לקרוא לשיטה אחרת שאנחנו מעבירים אליה. כלומר, כדי שתהיה לנו אפשרות להעביר התקשרות חוזרת אחת במקרים מסוימים, ואחרת במקרים אחרים. והשיטה שלנו, שתקבל את ההתקשרות שלנו, תתקשר אליהם. דוגמה פשוטה היא מיון. נניח שנכתוב איזשהו מיון מסובך שנראה בערך כך:
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
איפה, ifאנחנו קוראים לשיטה compare(), מעבירים שם שני אובייקטים שאנו משווים, ואנחנו רוצים לגלות איזה מהאובייקטים האלה הוא "גדול יותר". נשים את זה שהוא "יותר" לפני זה שהוא "קטן יותר". כתבתי "יותר" במרכאות כי אנחנו כותבים שיטה אוניברסלית שתהיה מסוגלת למיין לא רק בסדר עולה אלא גם בסדר יורד (במקרה הזה, "יותר" יהיה האובייקט שהוא בעצם קטן יותר, ולהיפך). . כדי להגדיר את הכלל בדיוק איך אנחנו רוצים למיין, אנחנו צריכים איכשהו להעביר אותו ל- שלנו mySuperSort(). במקרה זה, נוכל איכשהו "לשלוט" בשיטה שלנו בזמן שהיא נקראת. כמובן שניתן לכתוב שתי שיטות נפרדות mySuperSortAsc()למיון mySuperSortDesc()בסדר עולה ויורד. או להעביר פרמטר כלשהו בתוך השיטה (לדוגמה, booleanאם true, מיון בסדר עולה, ואם falseבסדר יורד). אבל מה אם נרצה למיין לא איזה מבנה פשוט, אלא, למשל, רשימה של מערכי מחרוזת? איך השיטה שלנו mySuperSort()תדע למיין את מערכי המחרוזות הללו? למדוד? לפי האורך הכולל של המילים? אולי בסדר אלפביתי, תלוי בשורה הראשונה במערך? אבל מה אם, במקרים מסוימים, נצטרך למיין רשימה של מערכים לפי גודל המערך, ובמקרה אחר, לפי האורך הכולל של המילים במערך? אני חושב שכבר שמעתם על השוואות ועל כך שבמקרים כאלה אנחנו פשוט מעבירים אובייקט השוואה לשיטת המיון שלנו, בה אנו מתארים את הכללים לפיהם אנו רוצים למיין. מכיוון שהשיטה הסטנדרטית sort()מיושמת על אותו עיקרון כמו , mySuperSort()בדוגמאות אשתמש בשיטה הסטנדרטית sort().
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }
        for (String s : o2) {
            length2 += s.length();
        }
        return length1 - length2;
    }
};

arrays.sort(sortByLength);
תוֹצָאָה:
  1. אמא שטפה את המסגרת
  2. שלום העבודה עשויה
  3. אני ממש אוהב ג'אווה
כאן המערכים ממוינים לפי מספר המילים בכל מערך. מערך עם פחות מילים נחשב "קטן יותר". בגלל זה זה בא בהתחלה. זה שבו יש יותר מילים נחשב "יותר" ומסתיים בסוף. אם sort()נעביר משווה נוסף לשיטה (sortByWordsLength), התוצאה תהיה שונה:
  1. שלום העבודה עשויה
  2. אמא שטפה את המסגרת
  3. אני ממש אוהב ג'אווה
כעת המערכים ממוינים לפי המספר הכולל של אותיות במילים של מערך כזה. במקרה הראשון יש 10 אותיות, בשני 12, ובשלישי 15. אם אנחנו משתמשים רק במשוואה אחת, אז לא נוכל ליצור עבורו משתנה נפרד, אלא פשוט ליצור אובייקט של מחלקה אנונימית ממש ב- זמן קריאה לשיטה sort(). כמו זה:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
התוצאה תהיה זהה למקרה הראשון. משימה 1 . כתוב מחדש את הדוגמה הזו כך שהיא תמיין את המערכים לא בסדר עולה של מספר המילים במערך, אלא בסדר יורד. אנחנו כבר יודעים את כל זה. אנחנו יודעים להעביר אובייקטים למתודות, נוכל להעביר אובייקט זה או אחר למתודה בהתאם למה שאנחנו צריכים כרגע, ובתוך השיטה שבה נעביר אובייקט כזה, השיטה שעבורה כתבנו את היישום תיקרא . נשאלת השאלה: מה הקשר לביטויי למבדה? בהינתן שלמבדה היא אובייקט שמכיל שיטה אחת בדיוק. זה כמו אובייקט שיטה. שיטה עטופה באובייקט. פשוט יש להם תחביר מעט יוצא דופן (אבל עוד על זה בהמשך). בואו נסתכל שוב על הערך הזה
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
כאן אנחנו לוקחים את הרשימה שלנו arraysוקוראים לשיטה שלה sort(), שבה אנחנו מעבירים אובייקט השוואה בשיטה אחת בודדת compare()(לא משנה לנו איך קוראים לו, כי הוא היחיד באובייקט הזה, לא נפספס אותו). שיטה זו לוקחת שני פרמטרים, איתם אנו עובדים בהמשך. אם אתה עובד ב- IntelliJ IDEA , בטח ראית איך הוא מציע לך את הקוד הזה כדי לקצר משמעותית:
arrays.sort((o1, o2) -> o1.length - o2.length);
כך הפכו שש שורות לאחד קצר. 6 שורות נכתבו מחדש לאחת קצרה אחת. משהו נעלם, אבל אני מבטיח ששום דבר חשוב לא נעלם, והקוד הזה יעבוד בדיוק כמו בכיתה אנונימית. משימה 2 . גלה כיצד לשכתב את הפתרון לבעיה 1 באמצעות למבדה (כמוצא אחרון, בקש מ- IntelliJ IDEA להפוך את הכיתה האנונימית שלך ללמבדה).

בואו נדבר על ממשקים

בעצם, ממשק הוא רק רשימה של שיטות מופשטות. כאשר אנו יוצרים מחלקה ואומרים שהיא תטמיע איזשהו ממשק, עלינו לכתוב במחלקה שלנו יישום של המתודות הרשומות בממשק (או, כמוצא אחרון, לא לכתוב אותה, אלא להפוך את המחלקה למופשטת ). ישנם ממשקים עם הרבה שיטות שונות (לדוגמה List), יש ממשקים עם שיטה אחת בלבד (לדוגמה, אותו Comparator או Runnable). יש ממשקים ללא שיטה אחת בכלל (מה שנקרא ממשקי סמן, למשל Serializable). ממשקים אלה שיש להם רק שיטה אחת נקראים גם ממשקים פונקציונליים . ב-Java 8 הם אפילו מסומנים בביאור @FunctionalInterface מיוחד . מדובר בממשקים עם שיטה אחת בודדת שמתאימים לשימוש בביטויי למבדה. כפי שאמרתי למעלה, ביטוי למבדה הוא שיטה עטופה באובייקט. וכאשר אנו מעבירים אובייקט כזה למקום כלשהו, ​​אנו, למעשה, עוברים את השיטה הבודדת הזו. מסתבר שזה לא משנה לנו איך קוראים לשיטה הזו. כל מה שחשוב לנו זה הפרמטרים ששיטה זו לוקחת, ולמעשה קוד השיטה עצמו. ביטוי למבדה הוא בעצם. יישום ממשק פונקציונלי. איפה שאנחנו רואים ממשק עם שיטה אחת, זה אומר שאנחנו יכולים לשכתב מחלקה אנונימית כזו באמצעות lambda. אם לממשק יש יותר/פחות משיטה אחת, אז הביטוי למבדה לא יתאים לנו, ונשתמש במחלקה אנונימית, או אפילו רגילה. הגיע הזמן לחפור בלמבדה. :)

תחביר

התחביר הכללי הוא בערך כך:
(параметры) -> {тело метода}
כלומר, סוגריים, בתוכם נמצאים פרמטרי השיטה, "חץ" (אלה שני תווים ברצף: מינוס ומעלה), שלאחריהם גוף השיטה נמצא בסוגרים מסולסלים, כמו תמיד. הפרמטרים תואמים לאלה שצוינו בממשק בעת תיאור השיטה. אם ניתן להגדיר בצורה ברורה את סוג המשתנים על ידי המהדר (במקרה שלנו, זה ידוע בוודאות שאנחנו עובדים עם מערכי מחרוזות, כי זה Listמוקלד בדיוק לפי מערכי מחרוזות), אז סוג המשתנים String[]לא צריך להיות כתוב.
אם אינך בטוח, ציין את הסוג, ו-IDEA ידגיש אותו באפור אם אין בו צורך.
אתה יכול לקרוא עוד במדריך של Oracle , למשל. זה נקרא "הקלדת יעד" . ניתן לתת למשתנים כל שמות, לאו דווקא אלה שצוינו בממשק. אם אין פרמטרים, אז רק סוגריים. אם יש רק פרמטר אחד, רק שם המשתנה ללא סוגריים. סידרנו את הפרמטרים, עכשיו לגבי גוף ביטוי הלמבדה עצמו. בתוך הפלטה המתולתלת, כתוב את הקוד כמו לשיטה רגילה. אם כל הקוד שלך מורכב משורה אחת בלבד, אינך צריך לכתוב פלטה מסולסלת כלל (כמו באם ולולאות). אם הלמבדה שלך מחזירה משהו, אבל הגוף שלה מורכב משורה אחת, אין returnצורך לכתוב בכלל. אבל אם יש לך פלטה מתולתלת, אז, כמו בשיטה הרגילה, אתה צריך לכתוב במפורש return.

דוגמאות

דוגמה 1.
() -> {}
האפשרות הפשוטה ביותר. והכי חסר משמעות :) כי זה לא עושה כלום. דוגמה 2.
() -> ""
גם אפשרות מעניינת. זה לא מקבל כלום ומחזיר מחרוזת ריקה ( returnהושמטה כמיותר). אותו דבר, אבל עם return:
() -> {
    return "";
}
דוגמה 3. שלום עולם באמצעות למבדות
() -> System.out.println("Hello world!")
לא מקבל כלום, לא מחזיר כלום (אנחנו לא יכולים לשים returnלפני הקריאה System.out.println(), שכן סוג ההחזרה בשיטה println() — void), פשוט מציג כיתוב על המסך. אידיאלי להטמעת ממשק Runnable. אותה דוגמה מלאה יותר:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello world!")).start();
    }
}
או ככה:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello world!"));
        t.start();
    }
}
או שנוכל אפילו לשמור את ביטוי למבדה כאובייקט מסוג Runnable, ואז להעביר אותו לבנאי thread’а:
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello world!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
בואו נסתכל מקרוב על הרגע של שמירת ביטוי למבדה למשתנה. הממשק Runnableאומר לנו שלאובייקטים שלו חייבת להיות שיטה public void run(). לפי הממשק, שיטת הריצה לא מקבלת שום דבר כפרמטרים. וזה לא מחזיר כלום (void). לכן, כאשר כותבים כך, יווצר אובייקט בשיטה כלשהי שאינה מקבלת או מחזירה דבר. וזה די תואם את השיטה run()בממשק Runnable. זו הסיבה שהצלחנו להכניס את ביטוי הלמבדה הזה למשתנה כמו Runnable. דוגמה 4
() -> 42
שוב, הוא לא מקבל שום דבר, אבל מחזיר את המספר 42. ניתן למקם את ביטוי הלמבדה הזה במשתנה מסוג Callable, כי הממשק הזה מגדיר רק שיטה אחת, שנראית בערך כך:
V call(),
היכן Vהוא סוג ערך ההחזרה (במקרה שלנו int). בהתאם לכך, אנו יכולים לאחסן ביטוי למבדה כזה כדלקמן:
Callable<Integer> c = () -> 42;
דוגמה 5. למבדה במספר שורות
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
שוב, זהו ביטוי למבדה ללא פרמטרים וסוג ההחזר שלו void(שכן אין return). דוגמה 6
x -> x
כאן אנחנו לוקחים משהו למשתנה хומחזירים אותו. שימו לב שאם רק פרמטר אחד מתקבל, אז אין צורך לכתוב את הסוגריים מסביב. אותו דבר, אבל עם סוגריים:
(x) -> x
והנה האפשרות עם אפשרות מפורשת return:
x -> {
    return x;
}
או ככה, עם סוגריים ו return:
(x) -> {
    return x;
}
או עם ציון מפורש של הסוג (ובהתאם, בסוגריים):
(int x) -> x
דוגמה 7
x -> ++x
אנחנו מקבלים את זה хומחזירים אותו, אבל בשביל 1יותר. אתה יכול גם לכתוב אותו מחדש כך:
x -> x + 1
בשני המקרים, איננו מציינים סוגריים סביב הפרמטר, גוף השיטה והמילה return, מכיוון שאין צורך בכך. אפשרויות עם סוגריים והחזרה מתוארות בדוגמה 6. דוגמה 8
(x, y) -> x % y
אנו מקבלים חלק хו у, מחזירים את שארית החלוקה xעל ידי y. כבר נדרשים כאן סוגריים סביב פרמטרים. הם אופציונליים רק כאשר יש רק פרמטר אחד. ככה עם ציון מפורש של סוגים:
(double x, int y) -> x % y
דוגמה 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
אנו מקבלים אובייקט Cat, מחרוזת עם שם וגיל שלם. בשיטה עצמה, הגדרנו את השם והגיל שעברו לחתול. מכיוון שהמשתנה catשלנו הוא סוג התייחסות, האובייקט Cat מחוץ לביטוי למבדה ישתנה (הוא יקבל את השם והגיל שעברו בפנים). גרסה קצת יותר מסובכת שמשתמשת בלמבדה דומה:
public class Main {
    public static void main(String[] args) {
        // create a cat and print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // create lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

    private static <T extends WithNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Murzik", 3);
    }
}

interface WithNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends WithNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements WithNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
תוצאה: Cat{name='null', age=0} Cat{name='Murzik', age=3} כפי שאתה יכול לראות, בהתחלה לאובייקט Cat היה מצב אחד, אך לאחר שימוש בביטוי למבדה, המצב השתנה . ביטויי למדה עובדים היטב עם גנריות. ואם נצטרך ליצור מחלקה Dog, למשל, שתטמיע גם את WithNameAndAge, אז בשיטה main()נוכל לעשות את אותן פעולות עם Dog, מבלי לשנות כלל את ביטוי הלמבדה עצמו. משימה 3 . כתוב ממשק פונקציונלי עם שיטה שלוקחת מספר ומחזירה ערך בוליאני. כתוב יישום של ממשק כזה בצורה של ביטוי למבדה שחוזר trueאם המספר שעבר מתחלק ב-13 ללא שארית משימה 4 . כתוב ממשק פונקציונלי עם שיטה שלוקחת שתי מחרוזות ומחזירה את אותה מחרוזת. כתוב יישום של ממשק כזה בצורה של למבדה שמחזירה את המחרוזת הארוכה ביותר. משימה 5 . כתוב ממשק פונקציונלי עם שיטה שמקבלת שלושה מספרים שברים: a, b, cומחזירה את אותו מספר שבר. כתוב יישום של ממשק כזה בצורה של ביטוי למבדה שמחזיר אבחנה. מי שכח, D = b^2 - 4ac . משימה 6 . באמצעות הממשק הפונקציונלי ממשימה 5, כתוב ביטוי למבדה שמחזיר את תוצאת הפעולה a * b^c. פופולרי על ביטויי למבדה בג'אווה. עם דוגמאות ומשימות. חלק 2.
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION