JavaLambda表达式详解

O泡李华 7

Java Lambda 表达式详解

本章位置:第二阶段 Java 核心框架
前置知识:接口、匿名内部类、集合框架
下一篇:Stream 流
学习目标:掌握函数式接口、Lambda 语法、常用函数式接口、方法引用、变量捕获,以及 Lambda 在线程、集合排序和 Stream 中的应用。


一、Lambda 是什么

Lambda 表达式是 Java 8 引入的重要语法,主要用于:

简化函数式接口的实现

匿名内部类:

Runnable task = new Runnable() {

    @Override
    public void run() {

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

Lambda:

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

Lambda 不只是“少写代码”,更重要的是:

可以把一段行为作为参数传递给方法。


二、什么是函数式接口

函数式接口:

只有一个抽象方法的接口。

例如:

@FunctionalInterface
public interface Calculator {

    int calculate(
            int a,
            int b
    );
}

可以使用:

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

调用:

int result =
        add.calculate(
                10,
                20
        );

结果:

30

三、@FunctionalInterface

@FunctionalInterface

明确告诉编译器:

这个接口应该是函数式接口

如果增加第二个抽象方法:

void test();

编译器会报错。

注意:

default 方法
static 方法

不会破坏函数式接口。


四、Lambda 基本语法

基本形式:

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

无参数:

() -> {

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

一个参数:

name -> {

    System.out.println(
            name
    );
}

多个参数:

(
    a,
    b
) -> {

    return a + b;
}

五、Lambda 简写规则

完整:

(
    int a,
    int b
) -> {

    return a + b;
}

参数类型可以省略:

(
    a,
    b
) -> {

    return a + b;
}

只有一条返回表达式:

(
    a,
    b
) ->
        a + b

只有一个参数:

name ->
        System.out.println(
                name
        )

可以省略参数括号。


六、Lambda 和匿名内部类

匿名内部类:

Comparator<Student> comparator =
        new Comparator<Student>() {

            @Override
            public int compare(
                    Student a,
                    Student b
            ) {

                return Integer.compare(
                        a.getAge(),
                        b.getAge()
                );
            }
        };

Lambda:

Comparator<Student> comparator =
        (
            a,
            b
        ) ->
                Integer.compare(
                        a.getAge(),
                        b.getAge()
                );

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

只有:

函数式接口

才能使用 Lambda。

如果接口:

public interface Handler {

    void start();

    void stop();
}

有两个抽象方法,就不能:

Handler handler =
        () -> {};

Lambda 也不能直接:

继承普通类
继承抽象类

而匿名内部类可以。


八、Java 常用函数式接口

主要位于:

java.util.function

重点掌握:

Supplier<T>
Consumer<T>
Function<T,R>
Predicate<T>

九、Supplier

特点:

无参数
有返回值

核心方法:

T get();

例如:

Supplier<String> supplier =
        () ->
                "Hello";

String value =
        supplier.get();

典型场景:

生产数据
延迟创建对象
提供默认值

十、Consumer

特点:

一个参数
无返回值

核心:

void accept(
        T t
);

例如:

Consumer<String> consumer =
        name ->
                System.out.println(
                        name
                );

consumer.accept(
        "张三"
);

典型:

打印
保存
处理
发送

十一、Function<T,R>

特点:

一个参数
一个返回值

核心:

R apply(
        T t
);

例如:

Function<String, Integer>
        function =
        text ->
                text.length();

Integer length =
        function.apply(
                "Java"
        );

典型:

数据映射
类型转换
对象转换

十二、Predicate

特点:

一个参数
返回 boolean

核心:

boolean test(
        T t
);

例如:

Predicate<Integer> adult =
        age ->
                age >= 18;

使用:

adult.test(
        20
);

典型:

过滤
规则判断
参数校验

十三、其他常见函数式接口

BiFunction<T,U,R>
两个参数,一个返回值

BiConsumer<T,U>
两个参数,无返回值

BiPredicate<T,U>
两个参数,返回 boolean

UnaryOperator<T>
T -> T

BinaryOperator<T>
(T,T) -> T

例如:

BinaryOperator<Integer> add =
        (
            a,
            b
        ) ->
                a + b;

十四、基本类型专用接口

为了减少装箱拆箱,还有:

IntSupplier
IntConsumer
IntPredicate
IntFunction

Long...
Double...

例如:

IntPredicate positive =
        value ->
                value > 0;

十五、Lambda 与 Comparator

按年龄升序:

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

更推荐:

students.sort(
        Comparator.comparingInt(
                Student::getAge
        )
);

十六、reversed()

降序:

students.sort(
        Comparator
                .comparingInt(
                        Student::getAge
                )
                .reversed()
);

十七、thenComparing()

多字段排序:

students.sort(
        Comparator
                .comparingInt(
                        Student::getScore
                )
                .reversed()
                .thenComparing(
                        Student::getName
                )
);

十八、为什么不推荐直接 a - b

例如:

a - b

在极端整数情况下可能:

溢出

更推荐:

Integer.compare(
        a,
        b
);

十九、Lambda 与线程

匿名内部类:

new Thread(
        new Runnable() {

            @Override
            public void run() {

                System.out.println(
                        "线程执行"
                );
            }
        }
).start();

Lambda:

new Thread(
        () ->
                System.out.println(
                        "线程执行"
                )
).start();

二十、Lambda 与集合

List<String> names =
        List.of(
                "张三",
                "李四",
                "王五"
        );

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

二十一、方法引用

如果 Lambda 只是调用一个已有方法,可以用:

::

简化。

Lambda:

name ->
        System.out.println(
                name
        )

方法引用:

System.out::println

所以:

names.forEach(
        System.out::println
);

二十二、方法引用四种常见形式

类名::静态方法

对象::实例方法

类名::实例方法

类名::new

二十三、静态方法引用

Lambda:

text ->
        Integer.parseInt(
                text
        )

方法引用:

Integer::parseInt

二十四、对象实例方法引用

System.out::println

等价:

value ->
        System.out.println(
                value
        )

二十五、类名::实例方法

String::length

等价:

text ->
        text.length()

例如:

Function<String, Integer>
        length =
        String::length;

二十六、Student::getName

Function<Student, String>
        getName =
        Student::getName;

等价:

student ->
        student.getName()

二十七、构造方法引用

无参:

Supplier<Student> supplier =
        Student::new;

等价:

() ->
        new Student()

二十八、有参构造引用

假设:

public Student(
        String name
) {

}

可以:

Function<String, Student>
        creator =
        Student::new;

二十九、方法引用不是必须使用

如果:

Student::getName

不容易理解,可以保留:

student ->
        student.getName()

原则:

可读性优先

三十、变量捕获

Lambda 可以使用外部局部变量:

String prefix =
        "Hello";

Consumer<String> consumer =
        name ->
                System.out.println(
                        prefix
                        + " "
                        + name
                );

三十一、effectively final

被 Lambda 捕获的局部变量必须:

final
或实际上不再修改

例如:

String prefix =
        "Hello";

后面如果:

prefix =
        "Hi";

Lambda 又引用 prefix,

编译会失败。

这种未显式写 final、但实际上没有修改的变量叫:

effectively final
有效 final

三十二、成员变量可以修改

例如:

public class Counter {

    private int count;

    public void test() {

        Runnable task =
                () ->
                        count++;
    }
}

成员变量:

不受 effectively final 限制

但如果多线程同时修改:

要考虑线程安全

三十三、Lambda 中的 this

Lambda 中:

this

通常指:

外部类当前对象

Lambda 没有自己的独立 this。


三十四、匿名内部类中的 this

匿名内部类中:

this

通常代表:

匿名内部类对象本身

所以 Lambda 和匿名内部类:

this 语义不同

三十五、Predicate 组合

Predicate 提供:

and
or
negate

例如:

Predicate<Integer> adult =
        age ->
                age >= 18;

Predicate<Integer> under60 =
        age ->
                age < 60;

Predicate<Integer> workingAge =
        adult.and(
                under60
        );

三十六、Predicate.negate()

Predicate<Integer> notAdult =
        adult.negate();

三十七、Function.andThen()

Function<Integer, Integer>
        doubleValue =
        value ->
                value * 2;

Function<Integer, String>
        toText =
        value ->
                "结果:"
                + value;

Function<Integer, String>
        combined =
        doubleValue
                .andThen(
                        toText
                );

调用顺序:

先 doubleValue
再 toText

三十八、Function.compose()

f.compose(g)

表示:

先 g
再 f

与:

andThen

顺序相反。


三十九、Consumer.andThen()

Consumer<String> print =
        System.out::println;

Consumer<String> save =
        value ->
                System.out.println(
                        "保存:"
                        + value
                );

Consumer<String> both =
        print.andThen(
                save
        );

四十、集合 removeIf()

students.removeIf(
        student ->
                student.getScore()
                < 60
);

这里接收:

Predicate

四十一、List.replaceAll()

names.replaceAll(
        String::toUpperCase
);

这里接收:

UnaryOperator

四十二、Map.forEach()

map.forEach(
        (
            key,
            value
        ) ->
                System.out.println(
                        key
                        + "="
                        + value
                )
);

接收:

BiConsumer

四十三、computeIfAbsent()

Map<String, List<String>>
        map =
        new HashMap<>();

map.computeIfAbsent(
        "Java",
        key ->
                new ArrayList<>()
).add(
        "Lambda"
);

四十四、Lambda 与 Optional

optional.ifPresent(
        value ->
                System.out.println(
                        value
                )
);

可以:

optional.ifPresent(
        System.out::println
);

四十五、Lambda 与 Stream

后面 Stream 的:

filter
map
sorted
forEach
reduce

都会大量接收函数式接口。

例如:

students.stream()
        .filter(
                student ->
                        student.getScore()
                        >= 60
        )
        .map(
                Student::getName
        )
        .forEach(
                System.out::println
        );

四十六、filter() 需要什么

filter(...)

需要:

Predicate<T>

因为它要判断:

这个元素保不保留

四十七、map() 需要什么

map(...)

需要:

Function<T,R>

因为它要:

把一个元素转换成另一个元素

四十八、forEach() 需要什么

forEach(...)

需要:

Consumer<T>

因为它:

接收元素
执行操作
不需要返回值

四十九、把行为作为参数

这是 Lambda 真正重要的思想。

例如:

public static List<Student>
filterStudents(
        List<Student> students,
        Predicate<Student> condition
) {

    List<Student> result =
            new ArrayList<>();

    for (
        Student student :
        students
    ) {

        if (
            condition.test(
                    student
            )
        ) {

            result.add(
                    student
            );
        }
    }

    return result;
}

五十、传入不同规则

筛选及格:

filterStudents(
        students,
        student ->
                student.getScore()
                >= 60
);

筛选成年人:

filterStudents(
        students,
        student ->
                student.getAge()
                >= 18
);

筛选软件工程:

filterStudents(
        students,
        student ->
                "软件工程".equals(
                        student.getMajor()
                )
);

同一个方法:

可以复用不同规则

五十一、函数式编程思想

传统:

方法内部写死规则

函数式思路:

把规则作为行为参数传入

让程序更:

灵活
可组合
可复用

五十二、Lambda 与策略模式

传统策略模式需要:

Strategy 接口
多个实现类
传入不同策略对象

如果策略接口是函数式接口,

简单策略可以直接:

value ->
        ...

减少大量一次性实现类。


五十三、Lambda 与回调

例如:

public static void execute(
        Runnable callback
) {

    System.out.println(
            "开始执行"
    );

    callback.run();
}

调用:

execute(
        () ->
                System.out.println(
                        "执行结束"
                )
);

这就是:

回调

五十四、Lambda 与异步编程

例如:

CompletableFuture
        .supplyAsync(
                () ->
                        "查询结果"
        )
        .thenAccept(
                System.out::println
        );

Lambda 非常适合:

异步任务
回调链

五十五、Lambda 与受检异常

标准 Function:

R apply(T t)

没有:

throws IOException

所以不能直接写:

path ->
        Files.readString(
                path
        )

如果会抛受检异常,需要处理。

例如:

Function<Path, String> reader =
        path -> {

            try {

                return Files.readString(
                        path
                );

            } catch (
                    IOException e
            ) {

                throw new UncheckedIOException(
                        e
                );
            }
        };

五十六、Lambda 和重载歧义

如果有:

void run(
        Runnable task
)

void run(
        Callable<String> task
)

有些 Lambda 可能让编译器难以确定:

到底匹配哪个重载

可以显式声明:

Callable<String> task =
        () ->
                "hello";

run(task);

五十七、Lambda 类型推断

例如:

Comparator<Student> comparator =
        (
            a,
            b
        ) ->
                Integer.compare(
                        a.getAge(),
                        b.getAge()
                );

虽然没写:

Student a
Student b

编译器能从:

Comparator<Student>

推断。


五十八、常见错误:不是函数式接口

错误:

interface Demo {

    void a();

    void b();
}

不能:

Demo demo =
        () -> {};

五十九、常见错误:多个参数不加括号

错误:

a, b ->
        a + b

正确:

(
    a,
    b
) ->
        a + b

六十、常见错误:有大括号但忘 return

错误:

(
    a,
    b
) -> {

    a + b;
}

正确:

(
    a,
    b
) -> {

    return a + b;
}

或者:

(
    a,
    b
) ->
        a + b

六十一、常见错误:修改被捕获局部变量

int count = 0;

Runnable task =
        () ->
                System.out.println(
                        count
                );

count++;

编译失败。

因为 count 不再:

effectively final

六十二、常见错误:复杂代码全部塞进 Lambda

不推荐:

list.forEach(
        item -> {

            // 很长的业务逻辑
        }
);

更推荐:

list.forEach(
        this::processItem
);

复杂逻辑抽成:

具名方法

六十三、常见错误:为了短而牺牲可读性

Lambda 可以很短,

但:

越短不代表越好

核心标准:

是否容易理解

六十四、练习题 1:Runnable

分别用:

匿名内部类
Lambda

创建线程任务。


六十五、练习题 2:Calculator

定义:

@FunctionalInterface
interface Calculator {

    int calculate(
            int a,
            int b
    );
}

使用 Lambda 实现:

加
减
乘
除

六十六、练习题 3:Predicate

判断 Student:

是否及格

六十七、练习题 4:Function

把:

Student

转换成:

String

返回姓名。


六十八、练习题 5:Consumer

打印:

学生姓名 + 成绩

六十九、练习题 6:Supplier

使用:

Student::new

创建 Student。


七十、练习题 7:Comparator

分别:

年龄升序
成绩降序

排序。


七十一、练习题 8:多字段排序

要求:

成绩降序

成绩相同时
姓名升序

使用:

Comparator
        .comparing...
        .thenComparing(...)

七十二、练习题 9:方法引用

把:

name ->
        System.out.println(
                name
        )

改为:

System.out::println

七十三、练习题 10:自定义过滤器

实现:

filterStudents(
        List<Student>,
        Predicate<Student>
)

然后传入至少:

三个不同过滤规则

七十四、必须掌握的四个核心函数式接口

Supplier<T>
无参 → 有返回值

Consumer<T>
有参数 → 无返回值

Function<T,R>
有参数 → 有返回值

Predicate<T>
有参数 → boolean

七十五、方法引用必须掌握

类名::静态方法

对象::实例方法

类名::实例方法

类名::new

七十六、必须回答的问题

学完后应该能够回答:

1. Lambda 是什么?

2. 什么是函数式接口?

3. @FunctionalInterface 有什么作用?

4. Lambda 和匿名内部类有什么区别?

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

6. Supplier / Consumer / Function / Predicate 分别是什么?

7. 什么是方法引用?

8. Student::getName 属于什么方法引用?

9. 什么是 effectively final?

10. Lambda 中 this 指向谁?

11. 为什么 Stream 大量使用 Lambda?

12. Lambda 的核心价值是什么?

七十七、Lambda 知识结构

Lambda
│
├─ 函数式接口
│   ├─ Supplier
│   ├─ Consumer
│   ├─ Function
│   └─ Predicate
│
├─ 语法
│   ├─ 无参
│   ├─ 单参
│   ├─ 多参
│   └─ 返回值
│
├─ 方法引用
│   ├─ 类::静态方法
│   ├─ 对象::实例方法
│   ├─ 类::实例方法
│   └─ 类::new
│
├─ 变量捕获
│   └─ effectively final
│
└─ 应用
    ├─ Comparator
    ├─ Thread
    ├─ Collection
    ├─ Callback
    └─ Stream

七十八、从 Lambda 到 Stream

现在我们已经会写:

Predicate<Student> condition =
        student ->
                student.getScore()
                >= 60;

也会:

Function<Student, String> mapper =
        Student::getName;

下一篇 Stream 会把这些行为组合起来:

students.stream()
        .filter(
                student ->
                        student.getScore()
                        >= 60
        )
        .map(
                Student::getName
        )
        .forEach(
                System.out::println
        );

七十九、本章总结

Lambda 最核心的一句话:

Lambda 是函数式接口的一种简洁实现方式。

它的核心价值不只是:

减少匿名内部类代码

而是:

允许把行为作为参数传递

学习顺序:

函数式接口
↓
Lambda 基础语法
↓
Supplier / Consumer / Function / Predicate
↓
Comparator
↓
方法引用
↓
变量捕获
↓
集合与线程应用
↓
Stream

掌握 Lambda 以后,下一篇正式进入:

Java Stream 流详解

也就是 Java 8 以后集合处理非常重要的函数式编程能力。