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

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

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

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

קצת היסטוריה

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

לב העניין

ביטוי למבדה הוא סוג של פונקציה. אתה יכול להתייחס לזה כשיטת Java רגילה, אבל עם היכולת הייחודית לעבור לשיטות אחרות כטיעון. זה נכון. זה הפך להיות אפשרי להעביר לא רק מספרים, מחרוזות וחתולים לשיטות, אלא גם שיטות אחרות! מתי אולי נצטרך את זה? זה יעזור, למשל, אם נרצה להעביר שיטת התקשרות חוזרת כלשהי. כלומר, אם אנחנו צריכים שהשיטה שאנו קוראים לה שתהיה לנו היכולת לקרוא לשיטה אחרת שאנו מעבירים אליה. במילים אחרות, אז יש לנו את היכולת להעביר התקשרות חוזרת אחת בנסיבות מסוימות והתקשרות חוזרת אחרת באחרות. וכדי שהשיטה שלנו שמקבלת את ההתקשרות שלנו קוראת להם. מיון הוא דוגמה פשוטה. נניח שאנחנו כותבים איזה אלגוריתם מיון חכם שנראה כך:
public void mySuperSort() {
    // We do something here
    if(compare(obj1, obj2) > 0)
    // And then we do something here
}
בהצהרה if, אנו קוראים לשיטה compare(), העברת שני אובייקטים שיש להשוות, ואנו רוצים לדעת איזה מהאובייקטים הללו הוא "גדול יותר". אנו מניחים שה"גדול" בא לפני ה"פחות". שמתי "גדול" במרכאות, כי אנחנו כותבים שיטה אוניברסלית שתדע למיין לא רק בסדר עולה, אלא גם בסדר יורד (במקרה הזה, העצם "הגדול" יהיה בעצם החפץ "הפחות" , ולהיפך). כדי להגדיר את האלגוריתם הספציפי למיון שלנו, אנחנו צריכים מנגנון כלשהו שיעביר אותו לשיטה שלנו mySuperSort(). כך נוכל "לשלוט" בשיטה שלנו כאשר היא נקראת. כמובן, נוכל לכתוב שתי שיטות נפרדות - mySuperSortAscend()וכן mySuperSortDescend()- למיון בסדר עולה ויורד. או שנוכל להעביר ארגומנט כלשהו למתודה (לדוגמה, משתנה בוליאני; אם נכון, אז מיון בסדר עולה, ואם לא נכון, אז בסדר יורד). אבל מה אם נרצה למיין משהו מסובך כמו רשימה של מערכי מחרוזות? איך השיטה שלנו mySuperSort()תדע למיין את מערכי המחרוזות הללו? לפי מידה? לפי האורך המצטבר של כל המילים? אולי לפי האלפביתי על המחרוזת הראשונה במערך? ומה אם נצטרך למיין את רשימת המערכים לפי גודל המערך במקרים מסוימים, ולפי האורך המצטבר של כל המילים בכל מערך במקרים אחרים? אני מצפה שכבר שמעתם על השוואות ושבמקרה הזה פשוט נעביר לשיטת המיון שלנו אובייקט השוואה שמתאר את אלגוריתם המיון הרצוי. מכיוון שהשיטה הסטנדרטית sort()מיושמת על בסיס אותו עיקרון כמו mySuperSort(), אשתמש sort()בדוגמאות שלי.
String[] array1 = {"Dota", "GTA5", "Halo"};
String[] array2 = {"I", "really", "love", "Java"};
String[] array3 = {"if", "then", "else"};

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[]> sortByCumulativeWordLength = 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. Dota GTA5 Halo
  2. if then else
  3. I really love Java
כאן המערכים ממוינים לפי מספר המילים בכל מערך. מערך עם פחות מילים נחשב ל"פחות". לכן זה בא קודם. מערך עם יותר מילים נחשב ל"גדול יותר" וממוקם בסופו. אם נעביר גורם השוואה שונה לשיטה sort(), כגון sortByCumulativeWordLength, אז נקבל תוצאה שונה:
  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
כעת מערכי ה-are ממוינים לפי המספר הכולל של האותיות במילים של המערך. במערך הראשון, יש 10 אותיות, בשני - 12, ובשלישי - 15. אם יש לנו רק משווה בודד, אז אנחנו לא צריכים להכריז על משתנה נפרד עבורו. במקום זאת, אנחנו יכולים פשוט ליצור מחלקה אנונימית ממש בזמן הקריאה לשיטה sort(). משהו כזה:
String[] array1 = {"Dota", "GTA5", "Halo"};
String[] array2 = {"I", "really", "love", "Java"};
String[] array3 = {"if", "then", "else"};

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;
    }
});
כאן אנחנו לוקחים את רשימת המערכים שלנו וקוראים למתודה שלו sort(), אליה אנחנו מעבירים אובייקט השוואה בשיטה בודדת compare()(השם שלו לא משנה לנו - אחרי הכל, זו השיטה היחידה של האובייקט הזה, אז אנחנו לא יכולים לטעות). לשיטה זו יש שני פרמטרים שאיתם נעבוד. אם אתה עובד ב- IntelliJ IDEA, בטח ראית שהוא מציע לרכז את הקוד באופן משמעותי באופן הבא:
arrays.sort((o1, o2) -> o1.length - o2.length);
זה מפחית שישה שורות לאחד קצר. 6 שורות נכתבות מחדש כאחת קצרה. משהו נעלם, אבל אני מבטיח שזה לא היה משהו חשוב. הקוד הזה יעבוד בדיוק כמו שהוא יעבוד עם מחלקה אנונימית. משימה 2. חשבו על שכתוב הפתרון למשימה 1 באמצעות ביטוי למבדה (לפחות, בקש מ-IntelliJ IDEA להמיר את הכיתה האנונימית שלך לביטוי למבדה).

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

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

תחביר

התחביר הכללי הוא בערך כך:
(parameters) -> {method body}
כלומר, סוגריים המקיפים את פרמטרי השיטה, "חץ" (שנוצר על ידי מקף וסימן גדול מ-), ולאחר מכן גוף השיטה בסוגרים, כמו תמיד. הפרמטרים תואמים לאלה שצוינו בשיטת הממשק. אם סוגי משתנים יכולים להיקבע באופן חד משמעי על ידי המהדר (במקרה שלנו, הוא יודע שאנחנו עובדים עם מערכי מחרוזת, כי 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()שיטה. לפי הממשק, runהשיטה לא לוקחת פרמטרים. והוא לא מחזיר כלום, כלומר סוג ההחזר שלו הוא void. בהתאם לכך, הקוד הזה יוצר אובייקט עם שיטה שלא לוקחת או מחזירה כלום. זה מתאים באופן מושלם לשיטת Runnableהממשק run(). זו הסיבה שהצלחנו לשים את ביטוי הלמבדה הזה במשתנה 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) -> x
והנה דוגמה עם הצהרת החזרה מפורשת:
x -> {
    return x;
}
או ככה עם סוגריים והצהרת החזרה:
(x) -> {
    return x;
}
או עם ציון מפורש של הסוג (וכך בסוגריים):
(int x) -> x
דוגמה 7
x -> ++x
אנחנו לוקחים xומחזירים אותו, אבל רק לאחר הוספת 1. אתה יכול לשכתב את הלמבדה כך:
x -> x + 1
בשני המקרים, אנו משמיטים את הסוגריים מסביב לגוף הפרמטר והשיטה, יחד עם ההצהרה return, מכיוון שהם אופציונליים. גרסאות עם סוגריים והצהרת החזרה ניתנות בדוגמה 6. דוגמה 8
(x, y) -> 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חפץ, Stringשם וגיל אינט. בשיטה עצמה אנו משתמשים בשם ובגיל שעברו כדי להגדיר משתנים על החתול. מכיוון שהאובייקט שלנו catהוא סוג התייחסות, הוא ישתנה מחוץ לביטוי הלמבדה (הוא יקבל את השם והגיל שעבר). הנה גרסה קצת יותר מסובכת שמשתמשת בלמבדה דומה:
public class Main {

    public static void main(String[] args) {
        // Create a cat and display it to confirm that it is "empty"
        Cat myCat = new Cat();
        System.out.println(myCat);

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

        };

        // Call a method to which we pass the cat and lambda
        changeEntity(myCat, s);

        // Display the cat on the screen and see that its state has changed (it has a name and age)
        System.out.println(myCat);

    }

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

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

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

class Cat implements HasNameAndAge {
    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='Smokey', age=3}
כפי שאתה יכול לראות, Catלאובייקט היה מצב אחד, ואז המצב השתנה לאחר שהשתמשנו בביטוי למבדה. ביטויי למדה משתלבים בצורה מושלמת עם תרופות גנריות. ואם אנחנו צריכים ליצור Dogמחלקה שגם מיישמת את HasNameAndAge, אז נוכל לבצע את אותן פעולות בשיטה Dogמבלי main() לשנות את ביטוי הלמבדה. משימה 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