[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表达式中修改变量,所以代码第②行和第⑥行如果去掉注释会发生编译错误。