什么是 Lambda
我们知道,对于一个 Java 变量,我们可以给它赋一个“值”,然后可以用它做一些操作。
Integer a = 1;
String s = "Hello";
System.out.println(s + a);
如果你想给一个 Java 变量赋一段“代码”,该怎么办呢?例如,我想把右边的代码块赋给一个名为 codeBlock 的 Java 变量。
在 Java 8 之前,这是不可能的。但在 Java 8 出现后,可以使用 Lambda 特性来做到这一点。
以下就是最直观的写法:
实际上是不允许这样写的会编译失败,这里只是为了让大家方便理解
codeBlock = public void doSomething(String s) {
System.out.println(s);
}
这种写法不是很简洁。我们可以去掉一些无用的声明对代码进行简化。
codeBlock = public void doSomething(String s) {
System.out.println(s);
}
// 这里的 public 是多余的,因为在这个上下文中不需要访问修饰符。
codeBlock = void doSomething(String s) {
System.out.println(s);
}
// 函数名 doSomething 也是多余的,因为已经将函数体赋值给了 codeBlock。
codeBlock = void (String s) {
System.out.println(s);
}
// 编译器可以自行推断返回类型,这里不需要显式地写出 void。
codeBlock = (String s) {
System.out.println(s);
}
// 编译器可以自行推断输入参数类型,这里不需要显式地写出 String 类型。
codeBlock = (s) -> System.out.println(s);
这样,我们就将一段“代码”赋给了一个变量。而“这段代码”,或者说“赋给变量的这个函数”,就是一个 Lambda 表达式。
但这里还有一个问题,即变量 codeBlock 应该是什么类型呢?在 Java 8 中,所有 Lambda 类型都是一个接口,而 Lambda 表达式本身,也就是“这段代码”,需要是这个接口的一个实现。在我看来,这是理解 Lambda 的关键。简而言之,Lambda 表达式本身就是一个接口的实现。直接这么说可能还是有点让人困惑,所以我们继续举例。我们给上面的 codeBlock 添加一个类型:
codeBlock = (s)->System.out.println(s);
interface LambdaInterface {
public void doSomething(String s);
}
这种只有一个函数需要实现的接口称为“函数式接口”。为了防止后来的人给这个接口添加接口函数,导致有多个接口函数需要实现而变成“非函数式接口”,我们可以给这个接口添加一个声明@FunctionalInterface,这样其他人就不能给它添加新函数了。
@FunctionalInterface
interface LambdaInterface {
public void doSomething(String s);
}
这样,我们就得到了一个完整的 Lambda 表达式声明。
LambdaInterface codeBlock =(s)System.out.println(s);
Lambda 表达式的作用是什么
最直观的作用就是使代码极其简洁。我们可以比较一下 Lambda 表达式和传统 Java 对同一接口的实现:
interface LambdaInterface {
public void doSomething(String s);
}
// Java 8
LambdaInterface codeBlock = (s) -> System.out.println(s);
// Java 7
publicclass LambdaInterfaceImpl implements LambdaInterface {
@Override
public void doSomething(String s) {
System.out.println(s);
}
}
这两种写法本质上是等价的。但显然,Java 8 中的写法更优雅简洁。而且,由于 Lambda 可以直接赋给变量,我们可以直接将 Lambda 作为参数传递给函数,而 java7 必须有明确的接口实现和初始化定义:
// 定义了一个静态方法 useLambda,它接受一个 LambdaInterface 类型的参数和一个 String 类型的参数。
public static void useLambda(LambdaInterface lambdaInterface, String s) {
lambdaInterface.doSomething(s);
}
// Java 8
// 直接使用 Lambda 表达式调用 useLambda 方法。
useLambda(s -> System.out.println(s), "Hello");
// Java 7
// 定义了一个 LambdaInterface 接口和一个实现该接口的 LambdaInterfaceImpl 类。
interface LambdaInterface {
public void doSomething(String s);
}
publicclass LambdaInterfaceImpl implements LambdaInterface {
@Override
public void doSomething(String s) {
System.out.println(s);
}
}
// 实例化 LambdaInterfaceImpl 类,并将实例传递给 useLambda 方法。
LambdaInterface myLambdaInterface = new LambdaInterfaceImpl();
useLambda(myLambdaInterface, "Hello");
在某些情况下,这个接口实现只需要使用一次。Java 7 要求你定义一个接口然后实现它。相比之下,Java 8 的 Lambda 看起来干净得多。Lambda 结合了函数式接口库、forEach、stream()、方法引用等新特性,使代码更加简洁!我们直接看例子。
@Getter
@AllArgsConstructor
public static class Student {
private String name;
private Integer age;
}
List<Student> students = Arrays.asList(
new Student("Bob", 18),
new Student("Ted", 17),
new Student("Zeka", 18)
);
现在你需要打印出 students 中所有 18 岁学生的名字。
原始的 Lambda 写法:定义两个函数式接口,定义一个静态函数,调用静态函数并给参数赋值 Lambda 表达式。
@FunctionalInterface
interface AgeMatcher {
boolean match(Student student);
}
@FunctionalInterface
interface Executor {
boolean execute(Student student);
}
public static void matchAndExecute(List<Student> students, AgeMatcher matcher, Executor executor) {
for (Student student : students) {
if (matcher.match(student)) {
executor.execute(student);
}
}
}
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("Bob", 18),
new Student("Ted", 17),
new Student("zeka", 18)
);
matchAndExecute(students,
s -> s.getAge() == 18,
s -> System.out.println(s.getName())
);
}
这段代码实际上已经比较简洁了,但我们还能更简洁吗?当然可以,Java 8 中有一个函数式接口包,它定义了大量可能用到的函数式接口(java.util.function (Java Platform SE 8))。
因此,我们根本不需要在这里定义 AgeMatcher 和 Executor 这两个函数式接口。我们可以直接使用 Java 8 函数式接口包中的 Predicate(T) 和 Consumer(T),因为它们的一对接口定义实际上与 AgeMatcher/Executor 相同。
第一步简化:利用函数式接口
public static void matchAndExecute(List<Student> students, Predicate<Student> predicate, Consumer<Student> consumer) {
for (Student student : students) {
if (predicate.test(student)) {
consumer.accept(student);
}
}
}
matchAndExecute 中的 forEach 循环实际上很烦人。这里可以使用 Iterable 自带的 forEach 代替。forEach 本身可以接受一个 Consumer(T) 参数。
第二步简化:用 Iterable.forEach 代替 forEach 循环:
public static void matchAndExecute(List<Student> students, Predicate<Student> predicate, Consumer<Student> consumer) {
students.forEach(s -> {
if (predicate.test(s)) {
consumer.accept(s);
}
});
}
由于 matchAndExecute 实际上只是对 List 的一个操作,这里我们可以去掉 matchAndExecute,直接使用 stream() 特性来完成它。stream() 的几个方法接受 Predicate(T) 和 Consumer(T) 等参数(java.util.stream (Java Platform SE 8))。一旦你理解了上面的内容,stream() 就很容易理解,不需要进一步解释。
第三步简化:用 stream() 代替静态函数:
students.stream()
.filter(s -> s.getAge() == 18)
.forEach(s -> System.out.println(s.getName()));
与最初的 Lambda 写法相比代码量已经减少了非常多。但如果我们要求改为打印学生的所有信息,并且s -> System.out.println(s);那么我们可以使用方法引用来继续简化。所谓方法引用,就是用已经编写好的其他 Object/Class 的方法来代替 Lambda 表达式。格式如下:
第四步简化:可以在 forEach 中使用方法引用代替 Lambda 表达式:
students.stream()
.filter(s -> s.getAge() == 18)
.map(Student::getName)
.forEach(System.out::println);
这基本上是我能写出的最简洁的版本了。
关于 Java 中的 Lambda 还有一些需要讨论和学习的地方。例如,如何利用 Lambda 的特性进行并行处理等。总之,我只是给你一个大致的介绍,让你有个概念。网上有很多关于 Lambda 的相关教程,多读多练,随着时间的推移肯定能够掌握它。