Lambda表达式
Lambda表达式是Java 8的重要更新, 也是一个被广大开发者期待 已久的新特性。 Lambda表达式支持将代码块作为方法参数, Lambda表 达式允许使用更简洁的代码来创建只有一个抽象方法的接(这种接 口被称为函数式接口)的实例。
1.1、Lambda表达式入门
public interface Command {
void process(int element);
}
public class ProcessArray {
public void process(int[] target, Command cmd) {
for (int t : target) {
cmd.process(t);
}
}
}
public class CommandTest {
public static void main(String[] args) {
ProcessArray pa = new ProcessArray();
int[] target = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(target, new Command() {
public void process(int element) {
System.out.println("数组元素的平方是:" + element * element);
}
});
}
}
ProcessArray类的process()方法处理数组时,希望可以动态传入一段代码作为具体的处理行为,因此程序创建了一个匿名内部类实例来封装处理行为。从上面代码可以看出,用于封装处理行为的关键就是实现程序中的这个方法,如下:
public void process(int element) {
System.out.println("数组元素的平方是:" + element * element);
}
但为了向process()方法传入这代码, 程序不得不使用匿名内部类的语法来创建对象。
Lambda表达式完全可用于简化创建匿名内部类对象, 因此可将上面代码改为如下形式。
public class CommandTest2 {
public static void main(String[] args) {
ProcessArray pa = new ProcessArray();
int[] array = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(array, (int element) -> {
System.out.println("数组元素的平方是:" + element * element);
});
}
}
从上面程序中的粗体字代码可以看出, 这段粗体字代码与创建匿名内部类时需要实现的process(int element)方法完全相同, 只是不 需要new Xxx(){}这种烦琐的代码,不需要指出重写的方法名字,也不需要给出重写的方法的返回值类型—只要给出重写的方法括号以及括 号里的形参列表即可。
从上面介绍可以看出, 当使用Lambda表达式代替匿名内部类创建对象时, Lambda表达式的代码块将会代替实现抽象方法的方法体, Lambda表达式就相当一个匿名方法。
从上面语法格式可以看出, Lambda表达式的主要作用就是代替匿 名内部类的烦琐语法。它由三部分组成。
➢ 形参列表。形参列表允许省略形参类型。如果形参列表中只有 一个参数,甚至连形参列表的圆括号也可以省略。
➢ 箭头(->)。必须通过英文中画线和大于符号组成。
➢ 代码块。如果代码块只包含一条语句,Lambda表达式允许省略 代码块的花括号, 那么这条语句就不要用花括号表示语句结 束。 Lambda代码块只有一条return语句, 甚至可以省略return 关键字。 Lambda表达式需要返回值, 而它的代码块中仅有一条 省略了return的语句, Lambda表达式会自动返回这条语句的 值。
下面程序示范了Lambda表达式的几种简化写法。
interface Eatable {
void taste();
}
interface Flyable {
void fly(String weather);
}
interface Addable {
int add(int a, int b);
}
public class LambdaQs {
// 调用该方法需要Eatable对象
public void eat(Eatable e) {
System.out.println(e);
e.taste();
}
// 调用该方法需要Flyable对象
public void drive(Flyable f) {
System.out.println("我正在驾驶:" + f);
f.fly("【碧空如洗的晴日】");
}
// 调用该方法需要Addable对象
public void test(Addable add) {
System.out.println("5与3的和为:" + add.add(5, 3));
}
public static void main(String[] args) {
LambdaQs lq = new LambdaQs();
// 1——Lambda表达式的代码块只有一条语句,可以省略花括号。
lq.eat(() -> System.out.println("苹果的味道不错!"));
// 2——Lambda表达式的形参列表只有一个形参,省略圆括号
lq.drive(weather -> {
System.out.println("今天天气是:" + weather);
System.out.println("直升机飞行平稳");
});
// 3——Lambda表达式的代码块只有一条语句,省略花括号
// 代码块中只有一条语句,即使该表达式需要返回值,也可以省略return关键字。
lq.test((a, b) -> a + b);
}
}
上面程序中标号为"1"代码使用Lambda表达式相当于不带形参的匿名方法, 由于该Lambda表达式的代码块只有一行代码, 因此可以省略代码块的花括号;标号为"2"代码使用Lambda表达式相当于 只带一个形参的匿名方法, 由于该Lambda表达式的形参列表只有一个 形参, 因此省略了形参列表的圆括号;标号为"3"代码的Lambda表 达式的代码块中只有一行语句,这行语句的返回值将作为该代码块的返回值。
上面程序中的标号为"1"代码调用eat()方法,调用该方法需要 一个Eatable类型的参数,但实际传入的是Lambda表达式;标号为"2"代码调用drive()方法, 调用该方法需要一个Flyable类型的参数, 但实际传入的是Lambda表达式;标号为"3"字代码调用test()方法, 调用该方法需要一个Addable类型的参数,但实际传入的是Lambda表达 式。 但上面程序可以正常编译、运行, 这说明Lambda表达式实际上将 会被当成一个“任意类型”的对象,到底需要当成何种类型的对象, 这取决于运行环境的需要。下面将详细介绍Lambda表达式被当成何种对象。
1.2、Lambda表达式与函数式接口
Lambda表达式的类型,也被称为“目标类型(target type)”, Lambda 表 达 式 的 目 标 类 型 必 须 是 “ 函 数 式 接 口 ( functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式 接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
如果采用匿名内部类语法来创建函数式接口的实例, 则只需要实 现一个抽象方法, 在这种情况下即可采用Lambda表达式来创建对象, 该表达式创建出来的对象的目标类型就是这个函数式接口。 查询Java 8 的 API 文 档 , 可 以 发 现 大 量 的 函 数 式 接 口 , 例 如 : Runnable 、 ActionListener等接口都是函数式接口。
Java 8专门为函数式接口提供了@FunctionalInterface注解, 该注解通常放在接口定义前面,该注解对程序功能没有任何作用, 它用于告诉编译器执行更严格检查—检查该接口必须是函数式接口,否则编译器就会报错。
由于Lambda表达式的结果就是被当成对象,因此程序中完全可以使用Lambda表达式进行赋值,例如如下代码。
public class LambdaTest {
public static void main(String[] args) {
// Runnable接口中只包含一个无参数的方法
// Lambda表达式代表的匿名方法实现了Runnable接口中唯一的、无参数的方法
// 因此下面的Lambda表达式创建了一个Runnable对象
Runnable r = () -> {
for (var i = 0; i < 100; i++) {
System.out.println(i);
}
};
}
}
从上面代码可以看出, Lambda表达式实现的是匿名方法因此它只能实现特定函数式接口中的唯一方法。 这意味着Lambda表达 式有如下两个限制。
➢ Lambda表达式的目标类型必须是明确的函数式接口。
➢ Lambda表达式只能为函数式接口创建对象。Lambda表达式只能 实现一个方法, 因此它只能为只有一个抽象方法的接口(函数 式接口)创建对象。
关于上面第一点限制,看下面代码是否正确(程序清单同上)。
public class LambdaTest {
public static void main(String[] args) {
// 下面代码报错: 不兼容的类型: Object不是函数接口
Object obj = () -> {
for (var i = 0; i < 100; i++)
{
System.out.println(i);
}
};
}
}
从该错误信息可以看出, Lambda表达式的目标类型必须是明确的 函数式接口。 上面代码将Lambda表达式赋值给Object变量, 编译器只 能确定该Lambda表达式的类型为Object, 而Object并不是函数式接 口,因此上面代码报错。
为了保证Lambda表达式的目标类型是一个明确的函数式接口, 可 以有如下三种常见方式。
➢ 将Lambda表达式赋值给函数式接口类型的变量。
➢ 将Lambda表达式作为函数式接口类型的参数传给某个方法。
➢ 使用函数式接口对Lambda表达式进行强制类型转换。
因此,只要将上面代码改为如下形式即可。
public class LambdaTest {
public static void main(String[] args) {
Object obj = (Runnable)() -> {
for (var i = 0; i < 100; i++)
{
System.out.println(i);
}
};
}
}
上面代码中对Lambda表达式执行了强制类型转换, 这样就可以确定该表达式的目标类型为Runnable函数式接口。
需要说明的是, 同样的Lambda表达式的目标类型完全可能是变化 的—唯一的要求是, Lambda表达式实现的匿名方法与目标类型(函数 式接口)中唯一的抽象方法有相同的形参列表。
例如定义了如下接口:
@FunctionalInterface
interface FkTest {
void run();
}
public class LambdaTest {
public static void main(String[] args) {
// 同样的Lambda表达式可以被当成不同的目标类型,唯一的要求是:
// Lambda表达式的形参列表与函数式接口中唯一的抽象方法的形参列表相同
Object obj2 = (FkTest) () -> {
for (var i = 0; i < 100; i++) {
System.out.println();
}
};
}
}
上面的函数式接口中仅定义了一个不带参数的方法, 因此前面强制转型为Runnable的Lambda表达式也可强转为FkTest类型—因为 FkTest接口中的唯一的抽象方法是不带参数的,而该Lambda表达式也是不带参数的。
Java 8在java.util.function包下预定义了大量函数式接口,典型地包含如下4类接口。
➢ XxxFunction:这类接口中通常包含一个apply()抽象方法,该 方法对参数进行处理、转换(apply()方法的处理逻辑由Lambda 表达式来实现), 然后返回一个新的值。 该函数式接口通常用 于对指定数据进行转换处理。
➢ XxxConsumer:这类接口中通常包含一个accept()抽象方法, 该方法与XxxFunction接口中的apply()方法基本相似, 也负责 对参数进行处理,只是该方法不会返回处理结果。
➢ XxxxPredicate:这类接口中通常包含一个test()抽象方法, 该方法通常用来对参数进行某种判断(test()方法的判断逻辑 由Lambda表达式来实现),然后返回一个boolean值。该接口通 常用于判断参数是否满足特定条件,经常用于进行筛滤数据。
➢ XxxSupplier:这类接口中通常包含一个getAsXxx()抽象方 法,该方法不需要输入参数,该方法会按某种逻辑算法 (getAsXxx ()方法的逻辑算法由Lambda表达式来实现)返回一个数据。
综上所述,不难发现Lambda表达式的本质很简单, 就是使用简洁的语法来创建函数式接口的实例—这种语法避免了匿名内部类的烦琐。
1.3、方法引用与构造器引用
前面已经介绍过, 如果Lambda表达式的代码块只有一条代码, 程 序就可以省略Lambda表达式中代码块的花括号。 不仅如此, 如果 Lambda表达式的代码块只有一条代码, 还可以在代码块中使用方法引 用和构造器引用。
方法引用和构造器引用可以让Lambda表达式的代码块更加简洁。 方法引用和构造器引用都需要使用两个英文冒号。 Lambda表达式支持如表下图所示的几种引用方式。
1.3.1、引用类方法
先看第一种方法引用:引用类方法。例如,定义了如下函数式接口。
@FunctionalInterface
interface Converter {
Integer convert(String from);
}
public class MethodRefer {
public static void main(String[] args) {
// 下面代码使用Lambda表达式创建Converter对象
Converter converter1 = from -> Integer.valueOf(from);
// 方法引用代替Lambda表达式:引用类方法。
// 函数式接口中被实现方法的全部参数传给该类方法作为参数。
Converter converter1 = Integer::valueOf;
Integer val = converter1.convert("99");
System.out.println(val); // 输出整数99
}
}
对于上面的类方法引用,也就是调用Integer类的valueOf()类方 法来实现Converter函数式接口中唯一的抽象方法,当调用Converter 接 口 中 的 唯 一 的 抽 象 方 法 时 , 调 用 参 数 将 会 传 给 Integer 类 的 valueOf()类方法。
1.3.2、引用特定对象得实例方法
下面看第二种方法引用:引用特定对象的实例方法。先使用 Lambda表达式来创建一个Converter对象。
@FunctionalInterface
interface Converter {
Integer convert(String from);
}
public class MethodRefer {
public static void main(String[] args) {
// 下面代码使用Lambda表达式创建Converter对象
Converter converter2 = from -> "fkit.org".indexOf(from);
// 方法引用代替Lambda表达式:引用特定对象的实例方法。
// 函数式接口中被实现方法的全部参数传给该方法作为参数。
Converter converter2 = "fkit.org"::indexOf;
Integer value = converter2.convert("it");
System.out.println(value); // 输出2
}
}
对 于 上 面 的 实 例 方 法 引 用 ,也 就 是 调 用 “fkit.org” 对 象 的 indexOf()实例方法来实现Converter函数式接口中唯一的抽象方法,当调用Converter接口中的唯一的抽象方法时,调用参数将会传 给"fkit.org"对象的indexOf()实例方法。
1.3.3、引用某类对象得实例方法
下面看第三种方法引用:引用某类对象的实例方法。 例如, 定义 了如下函数式接口。
@FunctionalInterface
interface MyTest {
String test(String a, int b, int c);
}
public class MethodRefer {
public static void main(String[] args) {
// 下面代码使用Lambda表达式创建MyTest对象
MyTest mt = (a, b, c) -> a.substring(b, c);
// 方法引用代替Lambda表达式:引用某类对象的实例方法。
// 函数式接口中被实现方法的第一个参数作为调用者,
// 后面的参数全部传给该方法作为参数。
MyTest mt = String::substring;
String str = mt.test("Java I Love you", 2, 9);
System.out.println(str); // 输出:va I Lo
}
}
对 于 上 面 的 实 例 方 法 引 用 , 也 就 是 调 用 某 个 String 对 象 的 substring()实例方法来实现MyTest函数式接口中唯一的抽象方法,当 调用MyTest接口中的唯一的抽象方法时, 第一个调用参数将作为 substring()方法的调用者, 剩下的调用参数会作为substring()实例方法的调用参数。
1.3.4、引用构造器
下面看构造器引用。例如,定义了如下函数式接口。
@FunctionalInterface
interface YourTest {
JFrame win(String title);
}
public class MethodRefer {
public static void main(String[] args) {
// 下面代码使用Lambda表达式创建YourTest对象
YourTest yt = a -> new JFrame(a);
// 构造器引用代替Lambda表达式。
// 函数式接口中被实现方法的全部参数传给该构造器作为参数。
YourTest yt = JFrame::new;
JFrame jf = yt.win("我的窗口");
System.out.println(jf);
}
}
对于上面的构造器引用, 也就是调用某个JFrame类的构造器来实现YourTest函数式接口中唯一的抽象方法, 当调用YourTest接口中的唯一的抽象方法时, 调用参数将会传给JFrame构造器。 从上面程序中可以看出,调用YourTest对象的win()抽象方法时,实际只传入了一个 String类型的参数, 这个String类型的参数会被传给JFrame构造器这就确定了是调用JFrame类的、带一个String参数的构造器。
1.4、Lambda表达式与匿名内部类的联系和区别
Lambda表达式是匿名内部类的一种简化, 因此它可以部分取代匿名内部类的作用。
Lambda表达式与匿名内部类 存在如下相同点。
➢ Lambda 表 达 式 与 匿 名 内 部 类 一 样 , 都 可 以 直 接 访 问 “effectively final”的局部变量, 以及外部类的成员变量 (包括实例变量和类变量)。
➢ Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
Lambda表达式与匿名内部类主要存在如下区别。
➢ 匿名内部类可以为任意接口创建实例—不管接口包含多少个抽 象方法, 只要匿名内部类实现所有的抽象方法即可;但Lambda 表达式只能为函数式接口创建实例。
➢ 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达 式只能为函数式接口创建实例。
➢ 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默 认方法;但Lambda表达式的代码块不允许调用接口中定义的默 认方法。