了解如何在 Java 程序中使用 lambda 表达式和函数式编程技术
在 Java SE 8 之前,通常使用匿名类将功能传递给方法。这种做法混淆了源代码,使其难以理解。Java 8 通过引入 lambda 消除了这个问题。本教程首先介绍 lambda 语言功能,然后提供有关使用 lambda 表达式以及目标类型进行函数编程的更详细的介绍。您还将学习 lambda 如何与范围,局部变量,thisand super 关键字以及 Java 异常交互。
请注意,本教程中的代码示例与 JDK 12 兼容。
自己发现类型
在本教程中,我不会介绍您以前没有学过的任何非 lambda 语言功能,但是我将通过本系列之前没有讨论过的类型来演示 lambda。一个例子是 java.lang.Math 类。我将在以后的 Java 101 教程中介绍这些类型。现在,我建议阅读 JDK 12 API 文档以了解有关它们的更多信息。
下载 获取代码 下载本教程中示例应用程序的源代码。由 Jeff Friesen 为 JavaWorld 创建。
Lambdas:入门
甲 lambda 表达式(拉姆达)描述了一个代码块(一匿名功能可以传递到构造或方法用于后续执行)。构造函数或方法接收 lambda 作为参数。考虑以下示例:
[在这个由 12 部分组成的综合课程中,从入门概念到高级设计模式学习 Java!]
() -> System.out.println("Hello")
此示例标识用于将消息输出到标准输出流的 lambda。从左到右,()标识 lambda 的形式参数列表(示例中没有参数),-> 表示该表达式是 lambda,并且 System.out.println(“Hello”)是要执行的代码。
Lambda 简化了功能性接口的使用,它们是带注释的接口,每个接口都精确地声明一个抽象方法(尽管它们也可以声明默认,静态和私有方法的任意组合)。例如,标准类库提供 java.lang.Runnable 具有单个抽象 void run()方法的接口。该功能接口的声明如下所示:
@FunctionalInterfacepublic interface Runnable{public abstract void run();}
类库注释 Runnable 与 @FunctionalInterface,这是的一个实例
java.lang.FunctionalInterface 注释类型。FunctionalInterface 用于注释在 lambda 上下文中使用的那些接口。
Lambda 没有明确的接口类型。相反,编译器使用周围的上下文来推断指定 lambda 时要实例化的功能接口 -lambda 绑定到该接口。例如,假设我指定了以下代码片段,它将前一个 lambda 作为参数传递给 java.lang.Thread 类的 Thread(Runnable target)构造函数:
new Thread(() -> System.out.println("Hello"));
编译器确定将 lambda 传递给它,Thread(Runnable r)因为这是唯一满足 lambda 的构造 Runnable 函数:是一个函数接口,lambda 的空形式参数列表 () 与的空参数列表匹配 run(),并且返回类型(void)也一致。Lambda 绑定到 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
Lambda 可以大大简化您必须编写的源代码数量,并且还可以使源代码更易于理解。例如,如果没有 lambda,您可能会指定清单 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 和 Streams API
除了简化源代码外,lambda 在 Java 的面向功能的 Streams API 中也起着重要作用。它们描述了传递给各种 API 方法的功能单元。
深入 Java Lambda
为了有效地使用 lambda,您必须了解 lambda 表达式的语法以及目标类型的概念。您还需要了解 lambda 如何与范围,局部变量,thisand 和 super 关键字以及异常进行交互。我将在以下各节中介绍所有这些主题。
lambda 如何实现
Lambda 是根据 Java 虚拟机的 invokedynamic 指令和 java.lang.invokeAPI 来实现的。观看视频“Lambda:深入了解”以了解 Lambda 体系结构。
Lambda 语法
每个 lambda 都遵循以下语法:
(formal-parameter-list) -> {expression-or-statements}
的 formal-parameter-list 是逗号分隔的形式参数,它必须在运行时功能接口的一个抽象方法的参数相匹配的列表。如果省略它们的类型,则编译器将从使用 lambda 的上下文中推断出这些类型。考虑以下示例:
(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- 表达式或语句块(称为 lambda 的主体)。与基于表达式的主体不同,必须将基于语句的主体置于 open({)和 close(})大括号字符之间:
(double radius) -> Math.PI * radius * radiusradius -> {return Math.PI * radius * radius;}radius -> {System.out.println(radius); return Math.PI * radius * radius; }
第一个示例的基于表达式的 lambda 主体不必放在括号之间。第二个示例将基于表达式的主体转换为基于语句的主体,return 必须在其中指定以返回表达式的值。最后的示例演示了多个语句,没有括号就无法表达。
Lambda 实体和分号
请注意;,在前面的示例中不存在分号()。在每种情况下,lambda 主体都不会以分号终止,因为 lambda 并非语句。但是,在基于语句的 lambda 主体中,每个语句必须以分号结尾。
清单 3 提供了一个简单的应用程序,该应用程序演示了 lambda 语法;请注意,此清单建立在前两个代码示例的基础上。
清单 3. LambdaDemo.java(版本 3)
@FunctionalInterfaceinterface BinaryCalculator{double calculate(double value1, double value2);}@FunctionalInterfaceinterface 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); }}
清单 3 首先介绍 BinaryCalculator 和 UnaryCalculator 接口,它们的 calculate()方法分别对两个输入参数或单个输入参数执行计算。此清单还引入了一个 LambdaDemo 类,该类的 main()方法演示了这些功能接口。
功能接口在 static double calculate(BinaryCalculator calc, double v1, double v2)和 static double calculate(UnaryCalculator calc, double v)方法中进行了演示。Lambda 将代码作为数据传递给这些方法,这些方法作为 BinaryCalculator 或 UnaryCalculator 实例接收。
编译清单 3 并运行该应用程序。您应该观察以下输出:
18 + 36.5 = 54.50000089 / 2.9 = 30.689655-89 = -89.00000018 * 18 = 324.000000
目标类型
Lambda 与隐式目标类型关联,该目标类型标识 Lambda 绑定到的对象的类型。目标类型必须是从上下文推断出的功能接口,它将 lambda 限制为出现在以下上下文中:
- 变量声明
- 分配
- 退货声明
- 数组初始化器
- 方法或构造函数参数
- λ 体
- 三元条件表达式
- 演员表
清单 4 提供了一个演示这些目标类型上下文的应用程序。
清单 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 < files.length; i++) System.out.println(files[i]); // Target type #4: array initializer FileSystem fs = FileSystems.getDefault(); final PathMatcher matchers[] = {(path) -> path.toString().endsWith("txt"), (path) -> path.toString().endsWith("java") }; FileVisitor<Path> visitor; visitor = new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) {Path name = file.getFileName(); for (int i = 0; i < matchers.length; i++) {if (matchers[i].matches(name)) System.out.printf("Found matched file:'%s'.%n", file); } return FileVisitResult.CONTINUE; } }; Files.walkFileTree(Paths.get("."), visitor); // Target type #5: method or constructor arguments new Thread(() -> System.out.println("running")).start(); // Target type #6: lambda body (a nested lambda) Callable<Runnable> callable = () -> () -> System.out.println("called"); callable.call().run(); // Target type #7: ternary conditional expression boolean ascendingSort = false; Comparator<String> cmp; cmp = (ascendingSort) ? (s1, s2) -> s1.compareTo(s2) : (s1, s2) -> s2.compareTo(s1); List<String> 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<String>) () -> System.getProperty("user.name")); System.out.println(user); } static FileFilter getFilter(String ext) {return (pathname) -> pathname.toString().endsWith(ext); }}
若哪里有错误或您有更好的方法,请留言指出~
若要获取更多 Java 干货知识,请关注我~
谢谢~