เริ่มต้นด้วยนิพจน์แลมบ์ดาใน Java

ก่อน Java SE 8 โดยทั่วไปจะใช้คลาสที่ไม่ระบุชื่อเพื่อส่งผ่านฟังก์ชันไปยังเมธอด วิธีปฏิบัตินี้ทำให้ซอร์สโค้ดสับสนทำให้เข้าใจยากขึ้น Java 8 กำจัดปัญหานี้โดยการแนะนำ lambdas บทช่วยสอนนี้แนะนำคุณลักษณะภาษาแลมบ์ดาก่อนจากนั้นให้ข้อมูลเบื้องต้นโดยละเอียดเกี่ยวกับการเขียนโปรแกรมเชิงฟังก์ชันด้วยนิพจน์แลมบ์ดาพร้อมกับประเภทเป้าหมาย นอกจากนี้คุณยังจะได้เรียนรู้วิธีการ lambdas โต้ตอบกับขอบเขตตัวแปรท้องถิ่นthisและsuperคำหลักและข้อยกเว้น Java 

โปรดทราบว่าตัวอย่างโค้ดในบทช่วยสอนนี้เข้ากันได้กับ JDK 12

ค้นหาประเภทสำหรับตัวคุณเอง

ฉันจะไม่แนะนำคุณสมบัติภาษาที่ไม่ใช่แลมบ์ดาในบทช่วยสอนนี้ซึ่งคุณไม่เคยเรียนมาก่อน แต่ฉันจะสาธิตแลมบ์ดาผ่านประเภทที่ฉันไม่เคยพูดถึงก่อนหน้านี้ในชุดนี้ ตัวอย่างหนึ่งคือjava.lang.Mathชั้นเรียน ฉันจะแนะนำประเภทเหล่านี้ในบทเรียน Java 101 ในอนาคต สำหรับตอนนี้ฉันขอแนะนำให้อ่านเอกสาร JDK 12 API เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับพวกเขา

ดาวน์โหลดรับโค้ดดาวน์โหลดซอร์สโค้ดสำหรับแอปพลิเคชันตัวอย่างในบทช่วยสอนนี้ สร้างโดย Jeff Friesen สำหรับ 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 คุณอาจจะระบุรายชื่อ 2 เพิ่มเติม verbose 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 และ Streams API

นอกจากการลดความซับซ้อนของซอร์สโค้ด lambdas ยังมีบทบาทสำคัญใน Streams API ที่เน้นการทำงานของ Java อธิบายหน่วยของฟังก์ชันการทำงานที่ส่งผ่านไปยังวิธีการ API ต่างๆ

Java lambdas ในเชิงลึก

To use lambdas effectively, you must understand the syntax of lambda expressions along with the notion of a target type. You also need to understand how lambdas interact with scopes, local variables, the this and super keywords, and exceptions. I'll cover all of these topics in the sections that follow.

How lambdas are implemented

Lambdas are implemented in terms of the Java virtual machine's invokedynamic instruction and the java.lang.invoke API. Watch the video Lambda: A Peek Under the Hood to learn about lambda architecture.

Lambda syntax

Every lambda conforms to the following syntax:

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

The formal-parameter-list is a comma-separated list of formal parameters, which must match the parameters of a functional interface's single abstract method at runtime. If you omit their types, the compiler infers these types from the context in which the lambda is used. Consider the following examples:

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

Lambdas and var

Starting with Java SE 11, you can replace a type name with var. For example, you could specify (var a, var b).

You must specify parentheses for multiple or no formal parameters. However, you can omit the parentheses (although you don't have to) when specifying a single formal parameter. (This applies to the parameter name only--parentheses are required when the type is also specified.) Consider the following additional examples:

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.

รายการ 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); } }