ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] ***** # 第 18 章 Java 8函数式编程基础——Lambda表达式 函数式编程将程序代码看作数学中的函数,函数本身作为另一个函数的参数或返回值,即高阶函数。 ## 18.1 Lambda表达式概述 ### 18.1.1 从一个示例开始 为了理解Lambda表达式的概念,下面先从一个示例开始。 假设有这样的一个需求:设计一个通用方法,能够实现两个数值的加法和减法运算。Java中方法不能单独存在,必须定义在类或接口中,考虑是一个通用方法,可以设计一个数值计算接口,其中定义该通用方法,代码如下: ~~~ //Calculable.java文件 package com.a51work6; //可计算接口 public interface Calculable { // 计算两个int数值 int calculateInt(int a, int b); } ~~~ Calculable接口只有一个方法calculateInt,参数是两个int类型,返回值也是int类型。通过方法如下: ~~~ //HelloWorld.java文件 … /** * 通过操作符,进行计算 * @param opr 操作符 * @return 实现Calculable接口对象 */ public static Calculable calculate(char opr) { Calculable result; if (opr == '+') { // 匿名内部类实现Calculable接口,生成实例对象 result = new Calculable() { ① // 实现加法运算 @Override public int calculateInt(int a, int b) { ② return a + b; } }; } else { // 匿名内部类实现Calculable接口,生成实例对象 result = new Calculable() { ③ // 实现减法运算 @Override public int calculateInt(int a, int b) { ④ return a - b; } }; } return result; } ~~~ 通用方法calculate中的参数opr是运算符,返回值是实现Calculable接口对象。代码第①行和第③行都采用匿名内部类实现Calculable接口。代码第②行实现加法运算。代码第④行实现减法运算。 调用通用方法代码如下: ~~~ //HelloWorld.java文件 … public static void main(String[] args) { int n1 = 10; int n2 = 5; // 实现加法计算Calculable对象 Calculable f1 = calculate('+'); ① // 实现减法计算Calculable对象 Calculable f2 = calculate('-'); ② // 调用calculateInt方法进行加法计算 System.out.printf("%d + %d = %d \n", n1, n2, f1.calculateInt(n1, n2)); ③ // 调用calculateInt方法进行减法计算 System.out.printf("%d - %d = %d \n", n1, n2, f2.calculateInt(n1, n2)); ④ } ~~~ 代码第①行中f1是实现加法计算Calculable对象,代码第②行中f2是实现减法计算Calculable对象。代码第③行和第④行才进行方法调用。 ### 18.1.2 Lambda表达式实现 Java 8采用Lambda表达式可以替代匿名内部类。修改之后的通用方法calculate代码如下: lambda表达式用于生成只有一个方法的接口的实例对象,并重写该接口的方法。 ~~~ // HelloWorld.java文件 … /** * 通过操作符,进行计算 * @param opr 操作符 * @return 实现Calculable接口对象 */ public static Calculable calculate(char opr) { Calculable result; if (opr == '+') { // Lambda表达式实现Calculable接口 result = (int a, int b) -> { //1 return a + b; }; } else { // result是Calculable类,Lambda表达式实现Calculable接口,并返回实现Calculable接口的实例对象。 result = (int a, int b) -> { //2 return a - b; }; } return result; } ~~~ 代码第①行和第②行用Lambda表达式替代匿名内部类,可见代码变得简洁。 **Lambda表达式定义**:Lambda表达式是一个匿名函数(方法)代码块,可以作为表达式、方法参数和方法返回值。 Lambda表达式标准语法形式如下: ~~~ (参数列表) -> { //Lambda表达式体 } ~~~ 其中,Lambda表达式参数列表与接口中方法参数列表形式一样,Lambda表达式体实现接口方法。 ***** result是Calculable类,Lambda表达式实现Calculable接口,并返回实现Calculable接口的实例对象。 ### 18.1.3 函数式接口 Lambda表达式实现的接口是函数式接口,这种接口只能有一个方法。 如果接口中声明多个抽象方法,那么Lambda表达式会发生编译错误: ~~~ The target type of this expression must be a functional interface ~~~ 这说明该接口不是函数式接口,为了防止在函数式接口中声明多个抽象方法,Java 8提供了一个声明函数式接口注解@FunctionalInterface,示例代码如下。 ~~~ //Calculable.java文件 package com.a51work6; //可计算接口 @FunctionalInterface public interface Calculable { // 计算两个int数值 int calculateInt(int a, int b); } ~~~ 在接口之前使用@FunctionalInterface注解修饰,那么试图增加一个抽象方法时会发生编译错误。但可以添加默认方法和静态方法。 > **提示** Lambda表达式是一个匿名方法代码,Java中的方法必须声明在类或接口中,那么Lambda表达式所实现的匿名方法是在函数式接口中声明的。 ## 18.2 Lambda表达式简化形式 使用Lambda表达式是为了简化程序代码,Lambda表达式本身也提供了多种简化形式,这些简化形式虽然简化了代码,但客观上使得代码可读性变差。本节介绍Lambda表达式本几种简化形式。 ### 18.2.1 省略参数类型 Lambda表达式可以根据上下文环境推断出参数类型,所以可以省略参数类型: ~~~ public static Calculable calculate(char opr) { Calculable result; if (opr == '+') { // Lambda表达式实现Calculable接口 result = (a, b) -> { //1 return a + b; }; } else { // Lambda表达式可以省略参数类型 result = (a, b) -> { //2 return a - b; }; } return result; } ~~~ 上述代码第①行和第②行的Lambda表达式是上一节示例的简化写法,其中a和b是参数。 ### 18.2.2 省略参数小括号 Lambda表达式中参数只有一个时,可以省略参数小括号。修改Calculable接口,代码如下。 ~~~ //Calculable.java文件 package com.a51work6; //可计算接口 @FunctionalInterface public interface Calculable { // 计算一个int数值 int calculateInt(int a); } ~~~ 其中calculateInt方法只有一个int类型参数,返回值也是int类型。调用代码如下: ~~~ //HelloWorld.java文件 package com.a51work6; public class HelloWorld { public static void main(String[] args) { int n1 = 10; // 实现二次方计算Calculable对象 Calculable f1 = calculate(2); // 实现三次方计算Calculable对象 Calculable f2 = calculate(3); // 调用calculateInt方法进行加法计算 System.out.printf("%d二次方 = %d \n", n1, f1.calculateInt(n1)); // 调用calculateInt方法进行减法计算 System.out.printf("%d三次方 = %d \n", n1, f2.calculateInt(n1)); } /** * 通过幂计算 * @param power 幂 * @return 实现Calculable接口对象 */ public static Calculable calculate(int power) { Calculable result; if (power == 2) { // Lambda表达式实现Calculable接口 result = (int a) -> { //标准形式 //1 return a * a; }; } else { // Lambda表达式实现Calculable接口 result = a -> { //函数式接口方法只有一个参数,lambda表达式省略了小括号 //2 return a * a * a; }; } return result; } } ~~~ 上述代码第①行和第②行都是实现Calculable接口的Lambda表达式。代码第①行是标准形式没有任何的简化。代码第②行省略了参数类型和小括号。 ### 18.2.3 省略return和大括号 如果Lambda表达式体中只有一条语句,那么可以省略return和大括号,代码如下: ~~~ public static Calculable calculate(int power) { Calculable result; if (power == 2) { // Lambda表达式实现Calculable接口 result = (int a) -> { //标准形式 return a * a; }; } else { // Lambda表达式实现Calculable接口,返回一个实例对象 result = a -> a * a * a; //函数式接口方法中只有一个参数和方法体只有一条语句,省略了小括号,花括号和return } return result; } ~~~ 上述代码第①行是省略了return和大括号,这是最简化形式的Lambda表达式。## 18.3 作为参数使用Lambda表达式 ## 18.3 作为参数使用Lambda表达式 Lambda表达式一种常见的用途是作为参数传递给方法。需要声明参数的类型声明为函数式接口类型。 示例代码如下: ~~~ //HelloWorld.java文件 package com.a51work6; public class HelloWorld { public static void main(String[] args) { int n1 = 10; int n2 = 5; // 打印计算结果加法计算结果,Lambda表达式传参 display((a, b) -> { return a + b; }, n1, n2); ① // 打印计算结果减法计算结果, Lambda表达式传参 display((a, b) -> a - b, n1, n2); ② } /** * 打印计算结果 * * @param calc Lambda表达式 * @param n1 操作数1 * @param n2 操作数2 * Calculable 是函数式接口,只有一个方法的接口 */ public static void display(Calculable calc, int n1, int n2) { ③ System.out.println(calc.calculateInt(n1, n2)); } } ~~~ 上述代码第③行定义display打印计算结果方法,其中参数calc类型是Calculable,这个参数即可以接收实现Calculable接口的对象,也可以接收Lambda表达式,因为Calculable是函数式接口。 代码第①行和第②行两次调用display方法,它们第一个参数都是Lambda表达式。 ## 18.4 访问变量 Lambda表达式可以访问所在外层作用域内定义的变量,包括:成员变量和局部变量。 ### 18.4.1 访问成员变量 成员变量包括:实例成员变量和静态成员变量。在Lambda表达式中可以访问这些成员变量,此时的Lambda表达式与普通方法一样,可以读取成员变量,也可以修改成员变量。 示例代码如下: ~~~ //LambdaDemo.java文件 package com.a51work6; public class LambdaDemo { // 实例成员变量 private int value = 10; // 静态成员变量 private static int staticValue = 5; // 静态方法,进行加法运算 public static Calculable add() { ① Calculable result = (int a, int b) -> { ② // 访问静态成员变量,不能访问实例成员变量 staticValue++; int c = a + b + staticValue; // this.value; return c; }; return result; } // 实例方法,进行减法运算 public Calculable sub() { ③ Calculable result = (int a, int b) -> { ④ // 访问静态成员变量和实例成员变量 staticValue++; this.value++; int c = a - b - staticValue - this.value; return c; }; return result; } } ~~~ LambdaDemo类中声明一个实例成员变量value和一个静态成员变量staticValue。此外,还声明了静态方法add(见代码第①行)和实例方法sub(见代码第③行)。add方法是静态方法,静态方法中不能访问实例成员变量,所以代码第②行的Lambda表达式中也不能访问实例成员变量,也不能访问实例成员方法。 sub方法是实例方法,实例方法中能够访问静态成员变量和实例成员变量,所以代码第④行的Lambda表达式中可以访问这些变量,当然实例方法和静态方法也可以访问,当访问实例成员变量或实例方法时可以使用this。 ### 18.4.2 捕获局部变量 对于成员变量的访问Lambda表达式与普通方法没有区别,但是对于访问外层局部变量时,会发生“捕获变量”情况。Lambda表达式中捕获变量时,会将变量当成final的,在Lambda表达式中不能修改那些捕获的变量。 示例代码如下: ~~~ //LambdaDemo.java文件 package com.a51work6; public class LambdaDemo { // 实例成员变量 private int value = 10; // 静态成员变量 private static int staticValue = 5; // 静态方法,进行加法运算 public static Calculable add() { //局部变量 int localValue = 20; ① Calculable result = (int a, int b) -> { // localValue++; //编译错误 ② int c = a + b + localValue; ③ return c; }; return result; } // 实例方法,进行减法运算 public Calculable sub() { //final局部变量 final int localValue = 20; ④ Calculable result = (int a, int b) -> { int c = a - b - staticValue - this.value; ⑤ // localValue = c; //编译错误 ⑥ return c; }; return result; } } ~~~ 上述代码第①行和第④行都声明一个局部变量localValue,Lambda表达式中捕获这个变量,见代码第③行和第⑤行。不管这个变量是否显式地使用final修饰,它都不能在Lambda表达式中修改变量,所以代码第②行和第⑥行如果去掉注释会发生编译错误。