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

לפני Java SE 8, שיעורים אנונימיים שימשו בדרך כלל להעברת פונקציונליות לשיטה. תרגול זה קוד מקור מעורפל, מה שמקשה על ההבנה. Java 8 ביטלה את הבעיה על ידי הצגת lambdas. מדריך זה מציג תחילה את תכונת שפת הלמבה, ואז מספק מבוא מפורט יותר לתכנות פונקציונאלי עם ביטויים למבדה יחד עם סוגי יעד. תוכל גם ללמוד כיצד lambdas אינטראקציה עם היקפים, משתנה מקומי, thisואת superמילות המפתח, וחריגי Java. 

שים לב כי דוגמאות קוד במדריך זה תואמות ל- JDK 12.

לגלות סוגים בעצמך

אני לא אציג שום תכונות בשפה שאינה למבדה במדריך זה שטרם למדת עליו, אך אדגים lambdas באמצעות סוגים שלא דיברתי עליהם בעבר בסדרה זו. דוגמה אחת היא java.lang.Mathהכיתה. אציג את הסוגים הללו בהדרכות עתידיות של Java 101. לעת עתה, אני מציע לקרוא את תיעוד ה- API של JDK 12 כדי ללמוד עוד עליהם.

הורד קבל את הקוד הורד את קוד המקור למשל יישומים במדריך זה. נוצר על ידי ג'ף פרייזן עבור JavaWorld.

Lambdas: פריימר

ביטוי למבדה (למבדה) מתאר בלוק של קוד (פונקציה אנונימית) כי ניתן להעביר בנאים או שיטות לביצוע שלאחר מכן. הקונסטרוקטור או השיטה מקבלים את הלמדה כטיעון. שקול את הדוגמה הבאה:

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

דוגמה זו מזהה למבדה להוצאת הודעה לזרם הפלט הסטנדרטי. משמאל לימין, ()מזהה את רשימת הפרמטרים הפורמלית של הלמדה (בדוגמה אין פרמטרים), ->מציין שהביטוי הוא למבדה, והוא System.out.println("Hello")הקוד שיש לבצע.

Lambdas מפשטים את השימוש בממשקים פונקציונליים , שהם ממשקי ביאור שכל אחד מהם מכריז בדיוק על שיטה מופשטת אחת (אם כי הם יכולים להכריז גם על כל שילוב של שיטות ברירת מחדל, סטטיות ופרטיות). לדוגמא, ספריית הכיתות הסטנדרטית מספקת java.lang.Runnableממשק עם void run()שיטה מופשטת אחת . הצהרת ממשק פונקציונלי זו מופיעה להלן:

@FunctionalInterface public interface Runnable { public abstract void run(); }

ספריית הכיתה מציינת Runnableעם @FunctionalInterface, שהיא מופע java.lang.FunctionalInterfaceמסוג ההערה. FunctionalInterfaceמשמש לביאור לאותם ממשקים שיש להשתמש בהקשרים למבדה.

למבדה אין סוג ממשק מפורש. במקום זאת, המהדר משתמש בהקשר הסובב כדי להסיק איזה ממשק פונקציונלי יוצר כאשר מצוין למבדה - הלמדה קשורה לממשק זה. לדוגמא, נניח שציינתי את קטע הקוד הבא, שמעביר את הלמדה הקודמת כטיעון לבנאי java.lang.Threadהכיתה Thread(Runnable target):

new Thread(() -> System.out.println("Hello"));

המהדר קובע כי העבירה למבדה Thread(Runnable r)מכיוון שזה הבנאי היחיד המספק את הלמדה: Runnableהוא ממשק פונקציונלי, רשימת הפרמטרים הפורמלית הריקה של הלמדה ()תואמת run()לרשימת הפרמטרים הריקה, וגם סוגי ההחזר ( void) מסכימים. למבדה חייב Runnable.

רישום 1 מציג את קוד המקור ליישום קטן המאפשר לך לשחק בדוגמה זו.

רישום 1. LambdaDemo.java (גרסה 1)

public class LambdaDemo { public static void main(String[] args) { new Thread(() -> System.out.println("Hello")).start(); } }

הידור רישום 1 ( javac LambdaDemo.java) והפעל את היישום ( java LambdaDemo). עליכם להתבונן בפלט הבא:

Hello

Lambdas יכול לפשט מאוד את כמות קוד המקור שעליכם לכתוב, וגם יכול להפוך את קוד המקור להרבה יותר קל להבנה. לדוגמא, ללא lambdas, כנראה היית מציין את הקוד הרחב יותר של Listing 2, המבוסס על מופע של מחלקה אנונימית המיישמת Runnable.

רישום 2. LambdaDemo.java (גרסה 2)

public class LambdaDemo { public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { System.out.println("Hello"); } }; new Thread(r).start(); } }

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

Lambdas ו- API של זרמים

בנוסף לפשט את קוד המקור, למבדי ממלאים תפקיד חשוב בממשק ה- API של Streams המכוון של Java. הם מתארים יחידות פונקציונליות המועברות לשיטות API שונות.

Lambdas Java לעומק

כדי להשתמש ביעילות למבדה עליך להבין את התחביר של ביטויי למבדה יחד עם הרעיון של סוג יעד. אתה צריך גם להבין כיצד lambdas אינטראקציה עם היקפים, משתנים מקומיים, thisואת superמילות מפתח, וחריגים. אסקור את כל הנושאים הללו בחלקים הבאים.

כיצד מיישמים lambdas

Lambdas מיושמים במונחים של הוראות המכונה הווירטואלית של Java invokedynamicו- java.lang.invokeAPI. צפו בסרטון למבה: הצצה מתחת למכסה המנוע כדי ללמוד על אדריכלות למבדה.

תחביר למבדה

כל למבדה תואם את התחביר הבא:

( formal-parameter-list ) -> { expression-or-statements }

formal-parameter-listהיא רשימה מופרדת בפסיקים של פרמטרים פורמלית, אשר חייב להתאים את הפרמטרים של השיטה המופשטת היחידה של ממשק פונקציונלי בזמן הריצה. אם תשמיט את הסוגים שלהם, המהדר מסיק את הסוגים האלה מההקשר שבו משתמשים במבדה. שקול את הדוגמאות הבאות:

(double a, double b) // types explicitly specified (a, b) // types inferred by compiler

Lambdas ו var

החל מ- Java SE 11, אתה יכול להחליף שם סוג ב- var. לדוגמה, תוכל לציין (var a, var b).

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

x // parentheses omitted due to single formal parameter (double x) // parentheses required because type is also present () // parentheses required when no formal parameters (x, y) // parentheses required because of multiple formal parameters

formal-parameter-listואחריו ->אסימון, אשר מלווה expression-or-statementsביטוי --an או בלוק של הצהרות (או ידוע בתור הגוף למבדה). בניגוד לגופים מבוססי-ביטוי, יש למקם גופים מבוססי-משפט בין תווים פתוחים ( {) וסגורים ( }):

(double radius) -> Math.PI * radius * radius radius -> { return Math.PI * radius * radius; } radius -> { System.out.println(radius); return Math.PI * radius * radius; }

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

גופי למבה ונקודות-נקודה

Note the absence or presence of semicolons (;) in the previous examples. In each case, the lambda body isn't terminated with a semicolon because the lambda isn't a statement. However, within a statement-based lambda body, each statement must be terminated with a semicolon.

Listing 3 presents a simple application that demonstrates lambda syntax; note that this listing builds on the previous two code examples.

Listing 3. LambdaDemo.java (version 3)

@FunctionalInterface interface BinaryCalculator { double calculate(double value1, double value2); } @FunctionalInterface interface UnaryCalculator { double calculate(double value); } public class LambdaDemo { public static void main(String[] args) { System.out.printf("18 + 36.5 = %f%n", calculate((double v1, double v2) -> v1 + v2, 18, 36.5)); System.out.printf("89 / 2.9 = %f%n", calculate((v1, v2) -> v1 / v2, 89, 2.9)); System.out.printf("-89 = %f%n", calculate(v -> -v, 89)); System.out.printf("18 * 18 = %f%n", calculate((double v) -> v * v, 18)); } static double calculate(BinaryCalculator calc, double v1, double v2) { return calc.calculate(v1, v2); } static double calculate(UnaryCalculator calc, double v) { return calc.calculate(v); } }

Listing 3 first introduces the BinaryCalculator and UnaryCalculator functional interfaces whose calculate() methods perform calculations on two input arguments or on a single input argument, respectively. This listing also introduces a LambdaDemo class whose main() method demonstrates these functional interfaces.

The functional interfaces are demonstrated in the static double calculate(BinaryCalculator calc, double v1, double v2) and static double calculate(UnaryCalculator calc, double v) methods. The lambdas pass code as data to these methods, which are received as BinaryCalculator or UnaryCalculator instances.

Compile Listing 3 and run the application. You should observe the following output:

18 + 36.5 = 54.500000 89 / 2.9 = 30.689655 -89 = -89.000000 18 * 18 = 324.000000

Target types

A lambda is associated with an implicit target type, which identifies the type of object to which a lambda is bound. The target type must be a functional interface that's inferred from the context, which limits lambdas to appearing in the following contexts:

  • Variable declaration
  • Assignment
  • Return statement
  • Array initializer
  • Method or constructor arguments
  • Lambda body
  • Ternary conditional expression
  • Cast expression

Listing 4 presents an application that demonstrates these target type contexts.

רישום 4. LambdaDemo.java (גרסה 4)

import java.io.File; import java.io.FileFilter; import java.nio.file.Files; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitor; import java.nio.file.FileVisitResult; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.Callable; public class LambdaDemo { public static void main(String[] args) throws Exception { // Target type #1: variable declaration Runnable r = () -> { System.out.println("running"); }; r.run(); // Target type #2: assignment r = () -> System.out.println("running"); r.run(); // Target type #3: return statement (in getFilter()) File[] files = new File(".").listFiles(getFilter("txt")); for (int i = 0; i  path.toString().endsWith("txt"), (path) -> path.toString().endsWith("java") }; FileVisitor visitor; visitor = new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) { Path name = file.getFileName(); for (int i = 0; i  System.out.println("running")).start(); // Target type #6: lambda body (a nested lambda) Callable callable = () -> () -> System.out.println("called"); callable.call().run(); // Target type #7: ternary conditional expression boolean ascendingSort = false; Comparator cmp; cmp = (ascendingSort) ? (s1, s2) -> s1.compareTo(s2) : (s1, s2) -> s2.compareTo(s1); List cities = Arrays.asList("Washington", "London", "Rome", "Berlin", "Jerusalem", "Ottawa", "Sydney", "Moscow"); Collections.sort(cities, cmp); for (int i = 0; i < cities.size(); i++) System.out.println(cities.get(i)); // Target type #8: cast expression String user = AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty("user.name")); System.out.println(user); } static FileFilter getFilter(String ext) { return (pathname) -> pathname.toString().endsWith(ext); } }