什么是 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);
}
当然,这并不是一种非常简洁的写法。所以,为了让这个赋值操作更加优雅,我们可以去掉一些没用的声明。
一个变量。而“这段代码”或“分配给变量的这个函数”是一个 Lambda 表达式。
但这里还是有一个问题,那就是变量 codeBlock 应该是什么类型呢?
在 Java 8 中,所有 Lambda 类型都是一个接口,Lambda 表达式本身(即“一段代码”)需要是此接口的实现。在我看来,这是理解 Lambda 的关键之一。简而言之,Lambda 表达式本身就是接口的实现。直接说这句话可能仍然有点令人困惑,所以让我们继续举个例子。我们在上面的 codeBlock 中添加一个类型:
这种只有一个功能要实现的接口称为 “功能接口”。
为了防止后来的人给这个接口添加接口功能,导致多个接口功能被实现,成为一个“非功能接口”,我们可以给这个接口添加一个声明@FunctionalInterface,这样别人就不能给它添加新的功能了。
通过这种方式,我们得到了一个完整的 Lambda 表达式声明。
Lambda 表达式的作用是什么?
最直观的效果是使代码极其简洁。
我们可以比较 Lambda 表达式和同一接口的传统 Java 实现:
这两种书写方式本质上是等价的。但显然,Java 8 的编写方式更加优雅和简洁。而且,由于 Lambda 可以直接分配给变量,因此我们可以直接将 Lambda 作为参数传递给函数,而传统的 Java 必须有明确的接口实现和初始化定义:
在某些情况下,此接口实现只需要使用一次。传统的 Java 7 需要定义一个 “污染环境” 接口来实现 InterfaceImpl。相比之下,Java 8 的 Lambda 看起来要简洁得多。
Lambda 结合了 FunctionalInterface Lib、forEach、stream()、方法引用和其他新功能,使代码更加简洁!
让我们直接来看这个例子。
假设给出了 Student 的定义和 List(Student) 的值。
@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 相同。
**步骤1:**简化 — 利用功能性接口包:
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 中的 for each 循环实际上非常烦人。在这里,你可以使用 Iterable 自己的 forEach() 来代替。forEach() 本身可以接受 Consumer(T) 参数。
**步骤2:**Simplify — 将 foreach 循环替换为 Iterable.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() 就很容易理解了,不需要任何进一步的解释。
**步骤3:**Simplify — 使用 stream() 而不是静态函数:
students.stream()
.filter(s -> s.getAge() == 18)
.forEach(s -> System.out.println(s.getName()));
相比于原来的 Lambda 写入方法,这非常简洁。但是,如果我们要求更改以打印学生的所有信息,以及 s -> System.out.println(s);那么我们可以使用 Method reference 来继续简化。所谓 Method 引用,就是将 Lambda 表达式替换为其他已编写的 Object/Class 的方法。格式如下:
**步骤4:**简化 – 您可以在 forEach 中使用方法引用而不是 Lambda 表达式:
students.stream()
.filter(s -> s.getAge() == 18)
.map(Student::getName)
.forEach(System.out::println);
关于 Java 中的 Lambda,如果大家感兴趣,我会在后续持续更新相关内容