Lambda 表达式基础 | JavaSE

Lambda 表达式基础

一、学习目标

完成本章后,你应该能够:

  • 能够解释 Lambda 表达式是什么,以及它解决了什么代码问题。
  • 能够写出 Lambda 表达式的基本语法结构。
  • 能够根据函数式接口的抽象方法确定 Lambda 的参数和返回值。
  • 能够分别手写无参数、有参数、有返回值和无返回值的 Lambda。
  • 能够判断某个匿名内部类能否使用 Lambda 表达式改写。
  • 能够理解 Lambda 本身没有独立类型,而是依赖目标函数式接口确定类型。

二、核心知识

2.1 什么是 Lambda 表达式

Lambda 表达式(Lambda Expression)是 Java 8 引入的一种语法形式,用于以更加简洁的方式表示一段可以被传递和执行的行为。

例如,一个函数式接口:

@FunctionalInterface
interface Swim {
    void swimming();
}

过去如果需要创建它的实现对象,可以使用匿名内部类:

Swim swim = new Swim() {
    @Override
    public void swimming() {
        System.out.println("游泳");
    }
};

使用 Lambda:

Swim swim = () -> {
    System.out.println("游泳");
};

可以发现,大量用于描述“我要实现哪个接口、我要重写哪个方法”的模板代码消失了。

Lambda 更关注的是:

这个行为到底要做什么。

而不是:

我要创建一个匿名类,然后实现接口,然后重写方法。


2.2 Lambda 的基本语法

Lambda 最核心的语法结构是:

(参数列表) -> {
    方法体
}

也可以理解为:

参数 -> 行为

其中:

左侧:参数
  ↓
(x, y) -> x + y
            ↑
          行为

箭头:

->

把参数与要执行的行为连接起来。


2.3 Lambda 与函数式接口的关系

Lambda 不能脱离接口独立存在。

例如:

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

可以写:

Calculator calculator = (int a, int b) -> {
    return a + b;
};

为什么编译器知道:

a
b

int

为什么知道 Lambda 最终需要返回 int

因为目标类型:

Calculator

已经规定:

int calculate(int a, int b);

因此可以建立对应关系:

Calculator

int calculate(int a, int b)
        ↓
(int a, int b) -> {
    return a + b;
}

Lambda 实际上是在为函数式接口的唯一抽象方法提供实现


2.4 Lambda 必须具有目标类型

下面的写法是不完整的:

var lambda = (a, b) -> a + b;

编译器无法仅根据 Lambda 自己确定:

  • a 是什么类型?
  • b 是什么类型?
  • 返回值是什么类型?
  • Lambda 最终表示什么接口?

因此 Lambda 通常出现在具有明确目标类型的上下文中。

例如赋值:

Calculator calculator = (a, b) -> a + b;

方法参数:

Arrays.sort(students, (a, b) -> Integer.compare(a.getAge(), b.getAge()));

或者显式类型转换:

Calculator calculator =
        (Calculator) ((a, b) -> a + b);

对于初学阶段,记住一句话:

Lambda 必须知道自己最终要成为哪个函数式接口的实现。


2.5 Lambda 能替代什么

Lambda 不能替代所有匿名内部类。

它主要用于:

函数式接口的实现。

例如:

interface Fly {
    void fly();
}

只有一个抽象方法,因此:

Fly fly = () -> {
    System.out.println("飞行");
};

合法。

但是:

interface Player {
    void play();

    void stop();
}

这里存在两个抽象方法,因此不能写:

Player player = () -> {
    System.out.println("...");
};

因为编译器不知道 Lambda 究竟实现:

play()

还是:

stop()

2.6 Lambda 不能直接替代抽象类匿名对象

例如:

abstract class Animal {
    public abstract void cry();
}

匿名内部类可以:

Animal animal = new Animal() {
    @Override
    public void cry() {
        System.out.println("喵喵喵");
    }
};

但不能改成:

Animal animal = () -> System.out.println("喵喵喵");

因为 Lambda 的目标类型必须是函数式接口类型

即使这个抽象类只有一个抽象方法,它也不是函数式接口。

所以:

匿名内部类
    ↓
既可以实现接口
也可以继承抽象类/普通类

Lambda
    ↓
只能用于函数式接口

这是两者的重要边界。


三、使用方法

3.1 无参数、无返回值

定义接口:

@FunctionalInterface
interface Task {
    void run();
}

使用 Lambda:

public class LambdaDemo01 {
    public static void main(String[] args) {
        Task task = () -> {
            System.out.println("执行任务");
        };

        task.run();
    }
}

@FunctionalInterface
interface Task {
    void run();
}

对应关系:

void run()

↓

() -> {
    System.out.println("执行任务");
}

没有参数时,必须保留:

()

3.2 一个参数、无返回值

接口:

@FunctionalInterface
interface Printer {
    void print(String message);
}

Lambda:

Printer printer = (String message) -> {
    System.out.println(message);
};

调用:

printer.print("星雨笔录");

完整程序:

public class LambdaDemo02 {
    public static void main(String[] args) {
        Printer printer = (String message) -> {
            System.out.println(message);
        };

        printer.print("星雨笔录");
    }
}

@FunctionalInterface
interface Printer {
    void print(String message);
}

3.3 多参数、无返回值

接口:

@FunctionalInterface
interface MessagePrinter {
    void print(String name, int age);
}

Lambda:

MessagePrinter printer = (String name, int age) -> {
    System.out.println("姓名:" + name);
    System.out.println("年龄:" + age);
};

调用:

printer.print("LingXi", 21);

注意:

Lambda 参数的数量必须与函数式接口抽象方法保持一致。


3.4 无参数、有返回值

接口:

@FunctionalInterface
interface NumberGenerator {
    int generate();
}

Lambda:

NumberGenerator generator = () -> {
    return 100;
};

调用:

int number = generator.generate();
System.out.println(number);

3.5 多参数、有返回值

这是最常见的形式之一。

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

使用:

public class LambdaDemo03 {
    public static void main(String[] args) {
        Calculator add = (int a, int b) -> {
            return a + b;
        };

        Calculator subtract = (int a, int b) -> {
            return a - b;
        };

        System.out.println(add.calculate(10, 20));
        System.out.println(subtract.calculate(10, 20));
    }
}

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

这里最重要的不是“Lambda 是一种奇怪的新方法”。

而是:

Calculator
     ↓
规定行为规格

Lambda
     ↓
提供行为实现

所以可以为同一个函数式接口提供不同实现:

Calculator add = (a, b) -> a + b;

Calculator subtract = (a, b) -> a - b;

Calculator multiply = (a, b) -> a * b;

这已经体现出函数式编程的重要思想:

行为也可以像数据一样被保存、传递和选择。


3.6 Lambda 作为方法参数

Lambda 真正有价值的地方之一,就是能够直接传递行为。

例如:

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

定义方法:

public static void calculate(
        int a,
        int b,
        Calculator calculator
) {
    int result = calculator.calculate(a, b);
    System.out.println(result);
}

调用:

calculate(10, 20, (a, b) -> a + b);

calculate(10, 20, (a, b) -> a * b);

完整程序:

public class LambdaDemo04 {

    public static void main(String[] args) {
        calculate(10, 20, (a, b) -> a + b);
        calculate(10, 20, (a, b) -> a - b);
        calculate(10, 20, (a, b) -> a * b);
    }

    public static void calculate(
            int a,
            int b,
            Calculator calculator
    ) {
        int result = calculator.calculate(a, b);
        System.out.println(result);
    }
}

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

此时:

(a, b) -> a + b

本质上就是作为参数被传入方法的一段行为。

这种思维在后面的:

  • Comparator
  • 集合 forEach
  • Stream
  • 线程
  • 事件监听

中会大量出现。


四、原理与进阶

4.1 Lambda 不是“没有名字的方法”

初学时可以把 Lambda 暂时理解成:

一段没有显式方法名的行为代码。

但从 Java 类型系统来看,更准确的理解是:

Lambda 表达式在函数式接口目标类型的上下文中,用于提供该接口函数方法的实现。

例如:

Calculator add = (a, b) -> a + b;

(a, b) -> a + b 自己并不能说明完整类型。

真正使它成立的是:

Calculator

4.2 Lambda 是多态表达式

Lambda 的具体类型受到上下文影响。

例如:

@FunctionalInterface
interface A {
    int calculate(int a, int b);
}

@FunctionalInterface
interface B {
    int calculate(int a, int b);
}

同样:

(a, b) -> a + b

既可能成为:

A

的实现,也可能成为:

B

的实现。

例如:

A a = (x, y) -> x + y;
B b = (x, y) -> x + y;

因此 Lambda 并不是看到:

(x, y) -> x + y

以后就拥有一个固定的、独立的类类型。

它依赖上下文提供目标类型。


4.3 创建 Lambda 与执行 Lambda 是两个阶段

例如:

Runnable task = () -> {
    System.out.println("Hello Lambda");
};

执行到这一行时,并不意味着:

System.out.println("Hello Lambda");

马上执行。

真正执行发生在:

task.run();

因此:

创建 Lambda
      ↓
获得函数式接口实例

调用函数式接口方法
      ↓
执行 Lambda 方法体

不要把:

Runnable task = () -> ...

理解成“立即执行右侧代码”。


五、实践应用

5.1 集合遍历

例如:

List<String> names =
        List.of("Java", "MySQL", "Spring");

传统增强 for

for (String name : names) {
    System.out.println(name);
}

Lambda 风格:

names.forEach(name -> {
    System.out.println(name);
});

这里:

name -> {
    System.out.println(name);
}

描述的是:

对每一个元素执行什么行为。


5.2 排序规则

过去:

Arrays.sort(students, new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return Integer.compare(
                o1.getAge(),
                o2.getAge()
        );
    }
});

Lambda:

Arrays.sort(
        students,
        (o1, o2) -> Integer.compare(
                o1.getAge(),
                o2.getAge()
        )
);

Lambda 将我们的注意力从:

创建 Comparator 匿名内部类
→ 重写 compare()
→ 编写比较规则

缩短为:

直接提供比较规则

5.3 线程任务

传统匿名内部类:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("执行线程任务");
    }
});

Lambda:

Thread thread = new Thread(() -> {
    System.out.println("执行线程任务");
});

这里 Thread 构造器需要 Runnable,而:

Runnable

是函数式接口,因此可以传入 Lambda。

这个例子以后学习多线程时还会再次遇到。


六、常见问题

6.1 Lambda 可以替代所有匿名内部类吗?

不能。

Lambda 的目标必须是函数式接口。

例如:

interface A {
    void test1();
    void test2();
}

存在两个抽象方法,不能使用 Lambda。


6.2 一个抽象类只有一个抽象方法,可以使用 Lambda 吗?

不能。

abstract class Animal {
    abstract void cry();
}

虽然只有一个抽象方法,但它不是接口,因此不是函数式接口。


6.3 Lambda 是一个普通方法吗?

不是。

例如:

(a, b) -> a + b

没有普通 Java 方法声明中的:

  • 方法名
  • 返回值类型声明
  • 修饰符

Lambda 是一种表达式。


6.4 为什么 Lambda 参数类型可以由编译器知道?

因为函数式接口已经规定参数类型。

例如:

interface Calculator {
    int calculate(int a, int b);
}

因此:

Calculator c = (a, b) -> a + b;

中的:

a
b

可以根据目标接口推断为 int

详细省略规则将在下一章专门学习。


6.5 Lambda 创建以后会马上执行吗?

不会。

Runnable task =
        () -> System.out.println("执行");

这里只是创建对应的函数式接口实例。

调用:

task.run();

时 Lambda 方法体才真正执行。


6.6 为什么下面代码无法确定类型?

// 错误示例
// var operation = (a, b) -> a + b;

因为 Lambda 需要目标函数式接口类型。

编译器缺少用于确定参数和返回值类型的目标类型。


七、练习与验收

7.1 知识问答

  1. 什么是 Lambda 表达式?
  2. Lambda 从哪个 Java 版本开始正式引入?
  3. Lambda 主要解决了哪类代码冗余问题?
  4. Lambda 的基本语法结构是什么?
  5. Lambda 为什么必须与函数式接口配合使用?
  6. 什么叫 Lambda 的目标类型?
  7. Lambda 可以替代任意匿名内部类吗?为什么?
  8. 一个只有一个抽象方法的抽象类为什么仍然不能直接使用 Lambda?
  9. Lambda 创建与 Lambda 方法体执行是否是同一件事?
  10. 为什么 (a, b) -> a + b 不能脱离上下文确定完整类型?

7.2 代码阅读

阅读:

@FunctionalInterface
interface Operation {
    int execute(int a, int b);
}

public class Test {
    public static void main(String[] args) {
        Operation operation = (int a, int b) -> {
            return a * b;
        };

        System.out.println(operation.execute(4, 5));
    }
}

回答:

  1. Lambda 对应的是哪个抽象方法?
  2. ab 分别是什么类型?
  3. Lambda 方法体什么时候真正执行?
  4. 最终输出什么?
  5. 如果 Operation 再增加一个抽象方法,该程序会发生什么?

阅读:

@FunctionalInterface
interface Task {
    void execute();
}

public class Test {
    public static void main(String[] args) {
        Task task = () -> {
            System.out.println("A");
        };

        System.out.println("B");
        task.execute();
    }
}

禁止运行程序,判断完整输出顺序,并解释原因。

7.3 手写代码

  1. 定义函数式接口 Printer
void print(String text);

使用 Lambda 输出字符串。

  1. 定义:
interface Calculator {
    int calculate(int a, int b);
}

分别使用 Lambda 实现:

  • 加法
  • 减法
  • 乘法
  • 除法
  1. 定义函数式接口:
interface Check {
    boolean test(int number);
}

分别实现:

  • 判断偶数
  • 判断正数
  • 判断是否大于 100
  1. 定义一个方法:
execute(int a, int b, Calculator calculator)

将计算规则作为参数传入。

7.4 Debug

下面代码有什么问题?

interface Action {
    void start();
    void stop();
}

public class Test {
    public static void main(String[] args) {
        Action action =
                () -> System.out.println("start");
    }
}

要求:

  1. 判断问题发生在编译期还是运行期。
  2. 说明错误原因。
  3. 给出至少一种合理的接口设计修复方案。

判断:

abstract class Animal {
    abstract void cry();
}

public class Test {
    public static void main(String[] args) {
        Animal animal =
                () -> System.out.println("喵");
    }
}

说明为什么不能编译。

7.5 综合训练

设计一个简易计算器。

要求:

Calculator add
Calculator subtract
Calculator multiply
Calculator divide

全部使用 Lambda 实现。

统一通过:

calculate(
        double a,
        double b,
        Calculator calculator
)

执行。

要求思考:

  • 为什么这里不需要创建四个 Calculator 实现类?
  • Lambda 在这个程序中究竟代表“数据”还是“行为”?
  • 如果未来增加取最大值操作,需要修改 Calculator 接口吗?

7.6 本章验收

闭卷完成以下任务:

  • 能用一句话解释 Lambda。
  • 能写出 Lambda 基本语法。
  • 能解释 Lambda 与函数式接口的关系。
  • 能判断一个接口能否使用 Lambda。
  • 能分别写出无参数、有参数、有返回值、无返回值 Lambda。
  • 能把简单函数式接口匿名内部类改成 Lambda。
  • 能解释为什么 Lambda 必须存在目标类型。

如果以上内容仍需要频繁查看语法模板,则本章暂时不能认为完全掌握。