Mulakan dengan ungkapan lambda di Jawa

Sebelum Java SE 8, kelas tanpa nama biasanya digunakan untuk menyampaikan fungsi ke suatu kaedah. Amalan ini mengaburkan kod sumber, menjadikannya sukar difahami. Java 8 menghilangkan masalah ini dengan memperkenalkan lambdas. Tutorial ini pertama kali memperkenalkan ciri bahasa lambda, kemudian memberikan pengenalan yang lebih terperinci untuk pengaturcaraan fungsional dengan ungkapan lambda bersama dengan jenis sasaran. Anda juga akan belajar bagaimana lambdas berinteraksi dengan skop, pembolehubah tempatan, thisdan superkata kunci, dan pengecualian Java. 

Perhatikan bahawa contoh kod dalam tutorial ini sesuai dengan JDK 12.

Mencari jenis untuk diri sendiri

Saya tidak akan memperkenalkan ciri bahasa bukan lambda dalam tutorial ini yang belum pernah anda pelajari sebelumnya, tetapi saya akan menunjukkan lambda melalui jenis yang belum pernah saya bincangkan dalam siri ini. Salah satu contohnya ialah java.lang.Mathkelas. Saya akan memperkenalkan jenis-jenis ini dalam tutorial Java 101 yang akan datang. Buat masa ini, saya cadangkan membaca dokumentasi API JDK 12 untuk mengetahui lebih lanjut mengenainya.

muat turun Dapatkan kod Muat turun kod sumber misalnya aplikasi dalam tutorial ini. Dicipta oleh Jeff Friesen untuk JavaWorld.

Lambdas: Permulaan

A ungkapan lambda (lambda) menerangkan satu blok kod (fungsi tanpa nama) yang boleh diserahkan kepada pengeluar atau kaedah bagi pelaksanaan berikutnya. Pembina atau kaedah menerima lambda sebagai hujah. Pertimbangkan contoh berikut:

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

Contoh ini mengenal pasti lambda untuk mengeluarkan mesej ke aliran output standard. Dari kiri ke kanan, ()mengenal pasti senarai parameter formal lambda (tidak ada parameter dalam contoh), ->menunjukkan bahawa ungkapan itu adalah lambda, dan System.out.println("Hello")merupakan kod yang akan dijalankan.

Lambdas mempermudah penggunaan antara muka fungsional , yang merupakan antarmuka beranotasi yang masing-masing menyatakan tepat satu kaedah abstrak (walaupun mereka juga dapat menyatakan kombinasi kaedah lalai, statik, dan swasta). Sebagai contoh, pustaka kelas standard menyediakan java.lang.Runnableantara muka dengan void run()kaedah abstrak tunggal . Pengisytiharan antara muka berfungsi ini terdapat di bawah:

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

Perpustakaan kelas diberi anotasi Runnabledengan @FunctionalInterface, yang merupakan contoh java.lang.FunctionalInterfacejenis anotasi. FunctionalInterfacedigunakan untuk memberi penjelasan antara muka yang akan digunakan dalam konteks lambda.

Lambda tidak mempunyai jenis antara muka yang jelas. Sebaliknya, penyusun menggunakan konteks di sekitarnya untuk menyimpulkan antara muka fungsional yang akan dibuat ketika lambda ditentukan - lambda terikat pada antara muka tersebut. Sebagai contoh, anggaplah saya menentukan pecahan kod berikut, yang menyampaikan lambda sebelumnya sebagai hujah kepada pembina java.lang.Threadkelas Thread(Runnable target):

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

Pengkompilasi menentukan bahawa lambda diserahkan Thread(Runnable r)kerana ini adalah satu-satunya konstruktor yang memenuhi lambda: Runnableadalah antara muka fungsional, senarai parameter formal kosong lambda ()sepadan run()dengan senarai parameter kosong, dan jenis pengembalian ( void) juga setuju. Lambda terikat dengan Runnable.

Penyenaraian 1 menunjukkan kod sumber ke aplikasi kecil yang membolehkan anda bermain dengan contoh ini.

Penyenaraian 1. LambdaDemo.java (versi 1)

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

Susun Penyenaraian 1 ( javac LambdaDemo.java) dan jalankan aplikasi ( java LambdaDemo). Anda harus memerhatikan output berikut:

Hello

Lambdas dapat mempermudah jumlah kod sumber yang mesti anda tulis, dan juga dapat membuat kod sumber lebih mudah difahami. Sebagai contoh, tanpa lambda, anda mungkin akan menentukan kod verbose Penyenaraian 2, yang berdasarkan pada contoh kelas tanpa nama yang dilaksanakan Runnable.

Penyenaraian 2. LambdaDemo.java (versi 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(); } }

Setelah menyusun kod sumber ini, jalankan aplikasi. Anda akan menemui output yang sama seperti yang ditunjukkan sebelumnya.

Lambdas dan API Aliran

Selain menyederhanakan kod sumber, lambdas memainkan peranan penting dalam API Aliran berorientasikan fungsi Java. Mereka menerangkan unit fungsi yang diteruskan ke pelbagai kaedah API.

Lambdas Jawa secara mendalam

Untuk menggunakan lambdas dengan berkesan, anda mesti memahami sintaks ungkapan lambda bersama dengan tanggapan jenis sasaran. Anda juga perlu memahami bagaimana lambdas berinteraksi dengan skop, pembolehubah tempatan, thisdan superkata kunci, dan pengecualian. Saya akan membahas semua topik ini di bahagian yang berikut.

Bagaimana lambdas dilaksanakan

Lambdas dilaksanakan dari segi invokedynamicinstruksi mesin maya Java dan java.lang.invokeAPI. Tonton video Lambda: A Peek Under the Hood untuk mengetahui mengenai seni bina lambda.

Sintaksis lambda

Setiap lambda mematuhi sintaks berikut:

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

Ini formal-parameter-listadalah senarai parameter formal yang dipisahkan koma, yang mesti sepadan dengan parameter kaedah abstrak tunggal antara muka fungsional pada waktu runtime. Sekiranya anda menghilangkan jenisnya, penyusun memasukkan jenis ini dari konteks di mana lambda digunakan. Pertimbangkan contoh berikut:

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

Lambdas dan var

Bermula dengan Java SE 11, anda boleh mengganti nama jenis dengan var. Sebagai contoh, anda boleh menentukan (var a, var b).

Anda mesti menentukan tanda kurung untuk beberapa atau tidak parameter formal. Namun, anda boleh menghilangkan tanda kurung (walaupun tidak perlu) ketika menentukan satu parameter formal. (Ini hanya berlaku untuk nama parameter - tanda kurung diperlukan apabila jenisnya juga ditentukan.) Pertimbangkan contoh tambahan berikut:

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

The formal-parameter-list is followed by a -> token, which is followed by expression-or-statements--an expression or a block of statements (either is known as the lambda's body). Unlike expression-based bodies, statement-based bodies must be placed between open ({) and close (}) brace characters:

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

The first example's expression-based lambda body doesn't have to be placed between braces. The second example converts the expression-based body to a statement-based body, in which return must be specified to return the expression's value. The final example demonstrates multiple statements and cannot be expressed without the braces.

Lambda bodies and semicolons

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.

Penyenaraian 4. LambdaDemo.java (versi 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); } }