Java 中的 Lambda

什么是 Lambda

我们知道,对于一个 Java 变量,我们可以给它赋一个"值",然后可以用它做一些操作。

java 复制代码
Integer a = 1;
String s = "Hello";
System.out.println(s + a);

如果你想给一个 Java 变量赋一段"代码",该怎么办呢?例如,我想把右边的代码块赋给一个名为 codeBlock 的 Java 变量。

在 Java 8 之前,这是不可能的。但在 Java 8 出现后,可以使用 Lambda 特性来做到这一点。

以下就是最直观的写法:

实际上是不允许这样写的会编译失败,这里只是为了让大家方便理解

java 复制代码
codeBlock = public void doSomething(String s) {
    System.out.println(s);
}

这种写法不是很简洁。我们可以去掉一些无用的声明对代码进行简化。

java 复制代码
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 添加一个类型:

java 复制代码
codeBlock = (s)->System.out.println(s);

interface LambdaInterface {
    public void doSomething(String s);
}

这种只有一个函数需要实现的接口称为"函数式接口"。为了防止后来的人给这个接口添加接口函数,导致有多个接口函数需要实现而变成"非函数式接口",我们可以给这个接口添加一个声明@FunctionalInterface,这样其他人就不能给它添加新函数了。

java 复制代码
@FunctionalInterface
interface LambdaInterface {
    public void doSomething(String s);
}

这样,我们就得到了一个完整的 Lambda 表达式声明。

java 复制代码
LambdaInterface codeBlock =(s)System.out.println(s);

Lambda 表达式的作用是什么

最直观的作用就是使代码极其简洁。我们可以比较一下 Lambda 表达式和传统 Java 对同一接口的实现:

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 必须有明确的接口实现和初始化定义:

java 复制代码
// 定义了一个静态方法 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()、方法引用等新特性,使代码更加简洁!我们直接看例子。

java 复制代码
@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 表达式。

java 复制代码
@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 相同。

第一步简化: 利用函数式接口

java 复制代码
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 循环:

java 复制代码
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() 代替静态函数:

java 复制代码
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 表达式:

java 复制代码
students.stream()
       .filter(s -> s.getAge() == 18)
       .map(Student::getName)
       .forEach(System.out::println);

这基本上是我能写出的最简洁的版本了。

关于 Java 中的 Lambda 还有一些需要讨论和学习的地方。例如,如何利用 Lambda 的特性进行并行处理等。总之,我只是给你一个大致的介绍,让你有个概念。网上有很多关于 Lambda 的相关教程,多读多练,随着时间的推移肯定能够掌握它。

相关推荐
蓝澈11219 分钟前
迪杰斯特拉算法之解决单源最短路径问题
java·数据结构
Kali_0716 分钟前
使用 Mathematical_Expression 从零开始实现数学题目的作答小游戏【可复制代码】
java·人工智能·免费
rzl0228 分钟前
java web5(黑马)
java·开发语言·前端
君爱学习34 分钟前
RocketMQ延迟消息是如何实现的?
后端
guojl1 小时前
深度解读jdk8 HashMap设计与源码
java
Falling421 小时前
使用 CNB 构建并部署maven项目
后端
guojl1 小时前
深度解读jdk8 ConcurrentHashMap设计与源码
java
程序员小假1 小时前
我们来讲一讲 ConcurrentHashMap
后端
爱上语文1 小时前
Redis基础(5):Redis的Java客户端
java·开发语言·数据库·redis·后端