JavaRush /בלוג Java /Random-HE /ביטויי למדה עם דוגמאות

ביטויי למדה עם דוגמאות

פורסם בקבוצה
Java היא בתחילה שפה מונחה עצמים לחלוטין. למעט טיפוסים פרימיטיביים, כל דבר בג'אווה הוא אובייקט. אפילו מערכים הם אובייקטים. מופעים של כל מחלקה הם אובייקטים. אין אפשרות אחת להגדיר פונקציה בנפרד (מחוץ למחלקה - בערך transl. ). ואין דרך להעביר שיטה כארגומנט או להחזיר גוף שיטה כתוצאה של שיטה אחרת. זה ככה. אבל זה היה לפני Java 8. ביטויי למדה עם דוגמאות - 1מאז ימי Swing הישן והטוב, היה צורך לכתוב שיעורים אנונימיים כאשר היה צורך להעביר פונקציונליות כלשהי לשיטה כלשהי. לדוגמה, כך נראתה הוספת מטפל באירועים:
someObject.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {

                //Event listener implementation goes here...

            }
        });
כאן אנחנו רוצים להוסיף קצת קוד למאזין האירועים בעכבר. הגדרנו מחלקה אנונימית MouseAdapterומיד יצרנו ממנה אובייקט. בדרך זו, העברנו פונקציונליות נוספת ל- addMouseListener. בקיצור, לא קל להעביר שיטה פשוטה (פונקציונליות) בג'אווה דרך ארגומנטים. מגבלה זו אילצה את מפתחי Java 8 להוסיף תכונה כגון ביטויי Lambda למפרט השפה.

למה Java צריך ביטויי Lambda?

מההתחלה, שפת ג'אווה לא התפתחה הרבה, למעט דברים כמו הערות, גנריות וכו'. קודם כל, ג'אווה תמיד נשארה מוכוונת עצמים. לאחר עבודה עם שפות פונקציונליות כמו JavaScript, ניתן להבין כיצד Java מונחה עצמים אך ורק מודפסת בצורה חזקה. אין צורך בפונקציות ב-Java. כשלעצמם, לא ניתן למצוא אותם בעולם ג'אווה. בשפות תכנות פונקציונליות, פונקציות באות לידי ביטוי. הם קיימים בפני עצמם. אתה יכול להקצות אותם למשתנים ולהעביר אותם דרך ארגומנטים לפונקציות אחרות. JavaScript היא אחת הדוגמאות הטובות ביותר לשפות תכנות פונקציונליות. אתה יכול למצוא מאמרים טובים באינטרנט המפרטים את היתרונות של JavaScript כשפה פונקציונלית. לשפות פונקציונליות יש כלים רבי עוצמה כגון Closure, המספקים מספר יתרונות על פני שיטות מסורתיות לכתיבת יישומים. סגירה היא פונקציה שמצורפת אליה סביבה - טבלה המאחסנת הפניות לכל המשתנים הלא מקומיים של הפונקציה. ב-Java, ניתן לדמות סגירות באמצעות ביטויי Lambda. כמובן שיש הבדלים בין סגירות לביטויי למבדה, ולא קטנים, אבל ביטויי למבדה הם אלטרנטיבה טובה לסגירות. בבלוג הסרקסטי והמצחיק שלו, סטיב יגה מתאר כיצד עולם ג'אווה קשור בקפדנות לשמות עצם (ישויות, אובייקטים - בערך תרגום ). אם לא קראת את הבלוג שלו, אני ממליץ עליו. הוא מתאר בצורה מצחיקה ומעניינת את הסיבה המדויקת שבגללה נוספו ביטויי למדה לג'אווה. ביטויי Lambda מביאים פונקציונליות לג'אווה שחסרה כל כך הרבה זמן. ביטויי למדה מביאים פונקציונליות לשפה בדיוק כמו אובייקטים. למרות שזה לא נכון ב-100%, אתה יכול לראות שביטויי Lambda, למרות שהם לא סגירות, מספקים יכולות דומות. בשפה פונקציונלית, ביטויי למבדה הם פונקציות; אבל בג'אווה, ביטויי למבדה מיוצגים על ידי אובייקטים, וחייבים להיות משויכים לסוג אובייקט ספציפי הנקרא ממשק פונקציונלי. בשלב הבא נסתכל במה מדובר. המאמר של מריו פוסקו "למה אנחנו צריכים ביטוי למבדה ב-Java" מתאר בפירוט מדוע כל השפות המודרניות זקוקות ליכולות סגירה.

מבוא לביטויי למדה

ביטויי Lambda הם פונקציות אנונימיות (ייתכן שאינה הגדרה נכונה ב-100% עבור Java, אבל היא מביאה בהירות מסוימת). במילים פשוטות, מדובר בשיטה ללא הצהרה, כלומר. ללא משנה גישה, החזרת ערך ושם. בקיצור, הם מאפשרים לכתוב שיטה ולהשתמש בה באופן מיידי. זה שימושי במיוחד במקרה של שיחת שיטה חד פעמית, מכיוון מפחית את הזמן שלוקח להכריז ולכתוב מתודה מבלי ליצור מחלקה. לביטויי למבדה ב-Java יש בדרך כלל את התחביר הבא (аргументы) -> (тело). לדוגמה:
(арг1, арг2...) -> { тело }

(тип1 арг1, тип2 арг2...) -> { тело }
להלן כמה דוגמאות לביטויי למדה אמיתיים:
(int a, int b) -> {  return a + b; }

() -> System.out.println("Hello World");

(String s) -> { System.out.println(s); }

() -> 42

() -> { return 3.1415 };

מבנה ביטויי למדה

בואו נלמד את המבנה של ביטויי למבדה:
  • לביטויי למדה יכולים להיות 0 או יותר פרמטרי קלט.
  • ניתן לציין במפורש את סוג הפרמטרים או לקבל מההקשר. לדוגמה ( int a) ניתן לכתוב כך ( a)
  • פרמטרים מוקפים בסוגריים ומופרדים בפסיקים. לדוגמה ( a, b) או ( int a, int b) או ( String a, int b, float c)
  • אם אין פרמטרים, עליך להשתמש בסוגריים ריקים. לדוגמה() -> 42
  • כאשר יש רק פרמטר אחד, אם הסוג לא מצוין במפורש, ניתן להשמיט את הסוגריים. דוגמא:a -> return a*a
  • הגוף של ביטוי למבדה יכול להכיל 0 ביטויים או יותר.
  • אם הגוף מורכב מהצהרה בודדת, ייתכן שהוא לא יהיה מוקף בסוגריים מסולסלים, וניתן לציין את ערך ההחזרה ללא מילת המפתח return.
  • אחרת, יש צורך בסוגרים המתולתלים (גוש קוד) ויש לציין את ערך ההחזרה בסוף באמצעות מילת מפתח return(אחרת סוג ההחזר יהיה void).

מהו ממשק פונקציונלי

ב-Java, ממשקי Marker הם ממשקים ללא הצהרת שיטות או שדות. במילים אחרות, ממשקי אסימונים הם ממשקים ריקים. באופן דומה, ממשקים פונקציונליים הם ממשקים עם שיטה מופשטת אחת בלבד המוצהרת עליהם. java.lang.Runnableהוא דוגמה לממשק פונקציונלי. הוא מצהיר רק על שיטה אחת void run(). יש גם ממשק ActionListener- גם פונקציונלי. בעבר, היינו צריכים להשתמש במחלקות אנונימיות כדי ליצור אובייקטים המיישמים ממשק פונקציונלי. עם ביטויי למדה, הכל הפך לפשוט יותר. כל ביטוי למבדה יכול להיות קשור באופן מרומז לממשק פונקציונלי כלשהו. לדוגמה, אתה יכול ליצור הפניה Runnableלממשק, כפי שמוצג בדוגמה הבאה:
Runnable r = () -> System.out.println("hello world");
המרה מסוג זה מתבצעת תמיד באופן מרומז כאשר איננו מציינים ממשק פונקציונלי:
new Thread(
    () -> System.out.println("hello world")
).start();
בדוגמה שלמעלה, המהדר יוצר אוטומטית ביטוי למבדה כיישום Runnableשל הממשק מבנאי המחלקה Thread: public Thread(Runnable r) { }. הנה כמה דוגמאות של ביטויי למבדה וממשקים פונקציונליים מתאימים:
Consumer<Integer> c = (int x) -> { System.out.println(x) };

BiConsumer<Integer, String> b = (Integer x, String y) -> System.out.println(x + " : " + y);

Predicate<String> p = (String s) -> { s == null };
ההערה @FunctionalInterfaceשנוספה ב-Java 8 לפי מפרט שפת Java בודקת אם הממשק המוצהר פונקציונלי. בנוסף, Java 8 כוללת מספר ממשקים פונקציונליים מוכנים לשימוש עם ביטויי Lambda. @FunctionalInterfaceיזרוק שגיאת קומפילציה אם הממשק המוצהר אינו פונקציונלי. להלן דוגמה להגדרת ממשק פונקציונלי:
@FunctionalInterface
public interface WorkerInterface {

    public void doSomeWork();

}
כפי שההגדרה מציעה, לממשק פונקציונלי יכולה להיות רק שיטה מופשטת אחת. אם תנסה להוסיף שיטה מופשטת נוספת, תקבל שגיאת קומפילציה. דוגמא:
@FunctionalInterface
public interface WorkerInterface {

    public void doSomeWork();

    public void doSomeMoreWork();

}
שְׁגִיאָה
Unexpected @FunctionalInterface annotation
    @FunctionalInterface ^ WorkerInterface is not a functional interface multiple
    non-overriding abstract methods found in interface WorkerInterface 1 error
После определения функционального интерфейса, мы можем его использовать и получать все преимущества Lambda-выражений. Пример:// defining a functional interface
@FunctionalInterface
public interface WorkerInterface {

    public void doSomeWork();

}
public class WorkerInterfaceTest {

    public static void execute(WorkerInterface worker) {
        worker.doSomeWork();
    }

    public static void main(String [] args) {

      // calling the doSomeWork method via an anonymous class
      // (classic)
      execute(new WorkerInterface() {
            @Override
            public void doSomeWork() {
               System.out.println("Worker called via an anonymous class");
            }
        });

      // calling the doSomeWork method via Lambda expressions
      // (Java 8 new)
      execute( () -> System.out.println("Worker called via Lambda") );
    }

}
סיכום:
Worker вызван через анонимный класс
Worker вызван через Lambda
כאן הגדרנו ממשק פונקציונלי משלנו והשתמשנו בביטוי למבדה. השיטה execute()מסוגלת לקבל ביטויי למבדה כטיעון.

דוגמאות לביטויי למדה

הדרך הטובה ביותר להבין ביטויי למבדה היא להסתכל על כמה דוגמאות: Threadניתן לאתחל זרם בשתי דרכים:
// Old way:
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello from thread");
    }
}).start();
// New way:
new Thread(
    () -> System.out.println("Hello from thread")
).start();
ניהול אירועים ב-Java 8 יכול להתבצע גם באמצעות ביטויי Lambda. להלן שתי דרכים להוסיף מטפל באירועים ActionListenerלרכיב ממשק משתמש:
// Old way:
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Button pressed. Old way!");
    }
});
// New way:
button.addActionListener( (e) -> {
        System.out.println("Button pressed. Lambda!");
});
דוגמה פשוטה להצגת כל האלמנטים של מערך נתון. שימו לב שיש יותר מדרך אחת להשתמש בביטוי למבדה. להלן אנו יוצרים ביטוי למבדה בדרך הרגילה באמצעות תחביר חץ, וכן אנו משתמשים באופרטור המעי הגס הכפול (::), אשר ב-Java 8 ממיר שיטה רגילה לביטוי למבדה:
// Old way:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for(Integer n: list) {
    System.out.println(n);
}
// New way:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n));
// New way using double colon operator ::
list.forEach(System.out::println);
בדוגמה הבאה, אנו משתמשים בממשק פונקציונלי Predicateכדי ליצור מבחן ולהדפיס פריטים שעוברים את המבחן הזה. כך תוכלו להכניס היגיון לביטויי למבדה ולעשות דברים על סמך זה.
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class Main {

    public static void main(String [] a)  {

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

        System.out.print("Outputs all numbers: ");
        evaluate(list, (n)->true);

        System.out.print("Does not output any number: ");
        evaluate(list, (n)->false);

        System.out.print("Output even numbers: ");
        evaluate(list, (n)-> n%2 == 0 );

        System.out.print("Output odd numbers: ");
        evaluate(list, (n)-> n%2 == 1 );

        System.out.print("Output numbers greater than 5: ");
        evaluate(list, (n)-> n > 5 );

    }

    public static void evaluate(List<Integer> list, Predicate<Integer> predicate) {
        for(Integer n: list)  {
            if(predicate.test(n)) {
                System.out.print(n + " ");
            }
        }
        System.out.println();
    }

}
סיכום:
Выводит все числа: 1 2 3 4 5 6 7
Не выводит ни одного числа:
Вывод четных чисел: 2 4 6
Вывод нечетных чисел: 1 3 5 7
Вывод чисел больше 5: 6 7
על ידי התעסקות עם ביטויי Lambda, תוכל להציג את הריבוע של כל אלמנט ברשימה. שימו לב שאנו משתמשים בשיטה stream()להמרת רשימה רגילה לזרם. Java 8 מספק מחלקה מדהימה Stream( java.util.stream.Stream). הוא מכיל טונות של שיטות שימושיות שאתה יכול להשתמש איתם בביטויי למבדה. אנו מעבירים ביטוי למבדה x -> x*xלשיטה map(), שמיישמת אותו על כל האלמנטים בזרם. לאחר מכן אנו משתמשים forEachכדי להדפיס את כל האלמנטים של הרשימה.
// Old way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer n : list) {
    int x = n * n;
    System.out.println(x);
}
// New way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map((x) -> x*x).forEach(System.out::println);
בהינתן רשימה, עליך להדפיס את סכום הריבועים של כל הרכיבים ברשימה. ביטויי למבדה מאפשרים לך להשיג זאת על ידי כתיבת שורת קוד אחת בלבד. דוגמה זו משתמשת בשיטת הקונבולציה (הפחתה) reduce(). אנו משתמשים בשיטה map()לריבוע כל אלמנט, ולאחר מכן משתמשים בשיטה reduce()כדי לכווץ את כל האלמנטים למספר בודד.
// Old way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = 0;
for(Integer n : list) {
    int x = n * n;
    sum = sum + x;
}
System.out.println(sum);
// New way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().map(x -> x*x).reduce((x,y) -> x + y).get();
System.out.println(sum);

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

ההבדל העיקרי הוא השימוש במילת המפתח this. עבור מחלקות אנונימיות, מילת המפתח ' ' thisמציינת אובייקט של המחלקה האנונימית, בעוד שבביטוי למבדה, ' this' מציינת אובייקט של המחלקה שבה נעשה שימוש בביטוי למבדה. הבדל נוסף הוא האופן שבו הם מורכבים. Java מרכיבה ביטויי למבדה וממירה אותם privateלשיטות מחלקות. זה משתמש בהוראה invokedynamic , שהוצגה ב-Java 7 עבור כריכת שיטה דינמית. טל וייס תיאר בבלוג שלו כיצד ג'אווה מרכיבה ביטויי למבדה לבייטקוד

סיכום

מארק ריינהולד (האדריכל הראשי של אורקל) כינה את ביטויי Lambda כשינוי המשמעותי ביותר במודל התכנות שאי פעם התרחש - אפילו משמעותי יותר מהגנרי. הוא בטח צודק, כי... הם נותנים למתכנתי Java את יכולות שפת התכנות הפונקציונליות שכולם חיכו להן. לצד חידושים כמו שיטות הרחבה וירטואלית, ביטויי Lambda מאפשרים לך לכתוב קוד באיכות גבוהה מאוד. אני מקווה שמאמר זה נתן לך מבט מתחת למכסה המנוע של Java 8. בהצלחה :)
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION