לפני Java SE 8, שיעורים אנונימיים שימשו בדרך כלל להעברת פונקציונליות לשיטה. תרגול זה קוד מקור מעורפל, מה שמקשה על ההבנה. Java 8 ביטלה את הבעיה על ידי הצגת lambdas. מדריך זה מציג תחילה את תכונת שפת הלמבה, ואז מספק מבוא מפורט יותר לתכנות פונקציונאלי עם ביטויים למבדה יחד עם סוגי יעד. תוכל גם ללמוד כיצד lambdas אינטראקציה עם היקפים, משתנה מקומי, this
ואת super
מילות המפתח, וחריגי Java.
שים לב כי דוגמאות קוד במדריך זה תואמות ל- JDK 12.
לגלות סוגים בעצמך
אני לא אציג שום תכונות בשפה שאינה למבדה במדריך זה שטרם למדת עליו, אך אדגים lambdas באמצעות סוגים שלא דיברתי עליהם בעבר בסדרה זו. דוגמה אחת היא java.lang.Math
הכיתה. אציג את הסוגים הללו בהדרכות עתידיות של Java 101. לעת עתה, אני מציע לקרוא את תיעוד ה- API של JDK 12 כדי ללמוד עוד עליהם.
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.invoke
API. צפו בסרטון למבה: הצצה מתחת למכסה המנוע כדי ללמוד על אדריכלות למבדה.
תחביר למבדה
כל למבדה תואם את התחביר הבא:
( 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); } }