Java(八):8.0 Java新语法总结

第一章:函数式数据处理

本章将系统性地介绍 Java 8 为开发者带来的 函数式编程范式,其核心是 Lambda 表达式Stream APIOptional 类型。学习本章,开发者将能够理解并掌握一套全新的、更高效的数据处理思想与工具,目标是将传统冗长、繁琐的 指令式代码,重构为现代化、简洁、且高度可读的 声明式代码


1.1. 项目设置与准备工作

在深入学习之前,需要先完成项目的基本设置。一个结构清晰、分层合理的项目是后续高效开发的基础。

1.1.1. 添加 Maven 依赖

本笔记中的部分便捷操作(如日期处理)依赖于 Hutool 工具库。首先,需要在项目的 pom.xml 文件中添加 hutool-all 依赖。

pom.xml 是 Maven 项目的核心配置文件,用于管理项目的依赖、插件、构建配置等。

文件路径: pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>modern-java-notes</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.29</version>
</dependency>
</dependencies>
</project>

1.1.2. 定义领域实体 (Entity)

我们将所有数据实体类(如 User, Role)都统一放在 entity 子包下,以实现业务模型的分离。

文件路径: src/main/java/com/example/notes/entity/Role.java

1
2
3
4
5
6
7
8
9
package com.example.notes.entity;

/**
* 用户角色枚举
*/
public enum Role {
ADMIN,
MEMBER
}

文件路径: src/main/java/com/example/notes/entity/User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.notes.entity;

import lombok.Value;
import java.time.LocalDateTime;

/**
* 用户数据类 (使用 Lombok 简化的 POJO)
* POJO: Plain Old Java Object
*/
@Value
// 使用@Value注解创建不可变的集合
public class User {
long id;
String name;
int age;
Role role;
LocalDateTime registrationDate;
}

1.1.3. 创建模拟数据源 (Data)

我们将提供模拟数据的功能放在 data 子包下。

文件路径: src/main/java/com/example/notes/data/UserDataSource.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.notes.data;

import cn.hutool.core.date.DateUtil;
import com.example.notes.entity.Role;
import com.example.notes.entity.User;
import java.util.List;

/**
* 模拟数据库,提供用户数据源
*/
public class UserDataSource {
public static List<User> getUsers() {
return List.of(
new User(1L, "Alice", 28, Role.ADMIN, DateUtil.parseLocalDateTime("2023-09-15 00:00:00")),
new User(2L, "Bob", 35, Role.MEMBER, DateUtil.parseLocalDateTime("2024-08-22 00:00:00")),
new User(3L, "Charlie", 22, Role.MEMBER, DateUtil.parseLocalDateTime("2025-03-10 00:00:00")),
new User(4L, "Diana", 42, Role.ADMIN, DateUtil.parseLocalDateTime("2023-11-05 00:00:00")),
new User(5L, "Ethan", 31, Role.MEMBER, DateUtil.parseLocalDateTime("2025-06-20 00:00:00")),
new User(6L, "Fiona", 29, Role.ADMIN, DateUtil.parseLocalDateTime("2024-02-15 00:00:00")));
}
}

1.2. 行为参数化:Lambda 表达式

行为参数化 指的是将代码块(即“行为”)作为方法的参数进行传递的能力。这是实现许多设计模式和函数式编程的基础。

1.2.1. 背景:匿名内部类的局限性

场景: 对用户列表按年龄进行升序排序。在 Java 8 之前,开发者必须通过匿名内部类来定义排序逻辑,语法十分繁琐。

文件路径: src/main/java/com/example/notes/ch01/LambdaIntroduction.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.notes.ch01;

import com.example.notes.data.UserDataSource;
import com.example.notes.entity.User;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class LambdaIntroduction {
public static void main(String[] args) {
// 必须拷贝一份,因为 Collections.sort 会修改原列表,而 List.of() 创建的是不可变列表
List<User> userList = new ArrayList<>(UserDataSource.getUsers());

// 排序行为:通过匿名内部类定义
Comparator<User> sortByAge = new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return Integer.compare(u1.getAge(), u2.getAge());
}
};

Collections.sort(userList, sortByAge);
System.out.println("按年龄排序后:");
for (User user : userList) {
System.out.println(user);
}
}
}

1.2.2. 核心概念:Lambda 表达式详解

Lambda 表达式提供了一种简洁、清晰的语法来表示一个匿名函数,其本质是函数式接口的一个实例。

核心语法结构: (parameters) -> { body }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Lambda 表达式的不同形式:

// 1. 标准形式
(User u1, User u2) -> { return u1.getAge() - u2.getAge(); }
// - 完整的参数类型、大括号和 return。

// 2. 类型推断 (常用)
(u1, u2) -> { return u1.getAge() - u2.getAge(); }
// - 省略参数类型,编译器可根据上下文推断。

// 3. 单行方法体 (简洁)
(u1, u2) -> u1.getAge() - u2.getAge()
// - 方法体只有一行 return 语句时,省略大括号和 return。

// 4. 单参数 (更简洁)
user -> user.getAge() > 30
// - 只有一个参数时,省略参数列表的圆括号 ()。

1.2.3. 实战:使用 Lambda 改造排序

利用 Lambda 表达式,之前的排序代码可以被极大地简化。

文件路径: src/main/java/com/example/notes/ch01/LambdaIntroduction.java (修改 main 方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.notes.ch01;

import cn.hutool.core.util.StrUtil;
import com.example.notes.data.UserDataSource;
import com.example.notes.entity.User;

import java.util.ArrayList;
import java.util.List;

public class LambdaIntroduction {
public static void main(String[] args) {
List<User> userList = new ArrayList<>(UserDataSource.getUsers());

userList.sort((u1,u2) -> u1.getAge() - u2.getAge());

System.out.println("使用 Lambda 按年龄排序后:");
// 使用 forEach 和 Lambda 遍历输出
userList.forEach(user -> System.out.println(user));
}
}


1.2.4. 核心契约:函数式接口

Lambda 表达式的类型由其上下文的目标类型决定,该目标类型必须是 函数式接口 (@FunctionalInterface)。

四大核心函数式接口

接口名抽象方法功能描述
Predicate<T>boolean test(T t)断言:接收参数,返回布尔值。
Function<T, R>R apply(T t)转换:接收参数,返回一个结果。
Supplier<T>T get()供给:不接收参数,返回一个结果。
Consumer<T>void accept(T t)消费:接收参数,无返回值。

实战演示

文件路径: src/main/java/com/example/notes/ch01/FunctionalInterfaceExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.example.notes.ch01;


import com.example.notes.entity.Role;
import com.example.notes.entity.User;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class FunctionalInterfaceExample {

public static void main(String[] args) {
User testUser = new User(1L, "Alice", 28, Role.ADMIN, LocalDateTime.now());

// 1. Predicate: 判断用户是否是管理员
Predicate<User> isAdmin = user -> Role.ADMIN.equals(user.getRole());
System.out.println("Alice 是管理员吗? " + isAdmin.test(testUser));

// 2. Function: 获取用户名
Function<User, String> getName = user -> user.getName();
System.out.println("用户的姓名是: " + getName.apply(testUser));

// 3. Consumer: 打印用户信息
Consumer<User> printUser = user -> System.out.println("正在消费用户: " + user);
printUser.accept(testUser);

// 4. Supplier: 创建一个“游客”用户
Supplier<User> guestFactory = () -> new User(0L, "Guest", 0, Role.MEMBER, LocalDateTime.now());
System.out.println("创建的游客用户: " + guestFactory.get());

}

}

1.2.5. 语法升华:方法引用

当 Lambda 表达式的方法体恰好是调用一个已存在的方法时,可以使用 方法引用 (::) 来进一步简化代码。

方法引用的四种类型

类型语法示例等价 Lambda
静态方法引用Integer::parseInts -> Integer.parseInt(s)
实例方法引用 (特定类型)String::toUpperCases -> s.toUpperCase()
实例方法引用 (特定对象)myUser::getName() -> myUser.getName()
构造函数引用ArrayList::new() -> new ArrayList<>()

实战: 使用方法引用再次优化排序代码。

文件路径: src/main/java/com/example/notes/ch01/LambdaIntroduction.java (再次修改 main 方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.notes.ch01;

import com.example.notes.data.UserDataSource;
import com.example.notes.entity.User;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class LambdaIntroduction {
public static void main(String[] args) {
List<User> userList = new ArrayList<>(UserDataSource.getUsers());

// 最终优化版本:使用方法引用,代码意图最清晰
userList.sort(Comparator.comparing(User::getAge));

System.out.println("使用 Lambda 按年龄排序后:");
// 使用 forEach 和 Lambda 遍历输出
userList.forEach(user -> System.out.println(user));
}
}


1.3. 声明式数据查询:Stream API

Stream API 是 Java 8 中对集合(Collection)操作的一次革命性升级。它引入了一种声明式、函数式的编程风格,允许我们以更优雅、更简洁的链式调用来执行复杂的数据查询和转换。

1.3.1. 背景:传统集合处理的痛点

在 Stream API 出现之前,处理集合数据通常意味着手写循环和使用临时变量。

场景: 从用户列表中,查找所有角色为 ADMIN 的用户,然后按年龄降序排序,最后提取他们的姓名。

传统方式的痛点:

  • 代码冗长: 需要编写 for 循环、if 条件判断。
  • 逻辑分散: 筛选、排序、提取的逻辑散落在不同代码块,不易阅读。
  • 状态管理复杂: 需要创建临时集合(如 adminUsers, adminNames)来存储中间结果,容易出错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 传统方式实现
List<User> adminUsers = new ArrayList<>();
for (User user : userList) {
if (Role.ADMIN.equals(user.getRole())) {
adminUsers.add(user);
}
}
Collections.sort(adminUsers, new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return Integer.compare(u2.getAge(), u1.getAge()); // 降序
}
});
List<String> adminNames = new ArrayList<>();
for (User user : adminUsers) {
adminNames.add(user.getName());
}

可以看到,仅仅一个简单的需求,就需要十几行代码和多个中间变量。Stream API 正是为了解决这些问题而生。


1.3.2. 核心理念:Stream 的设计哲学

Stream 将数据处理过程抽象为一条“流水线”(Pipeline),它由三个核心部分组成:

  1. 数据源: 流水线的起点。可以是集合、数组、I/O 通道等。通过调用 .stream() 方法从一个集合获取流。
  2. 中间操作: 对数据进行处理的环节,如筛选、排序、转换。每个中间操作都会返回一个新的 Stream,这使得操作可以链接起来。
  3. 终端操作: 流水线的终点。它会触发整个流水线的计算并产生最终结果,如生成一个 List、计算总数或打印每个元素。

此外,Stream 还有两个至关重要的特性:

  • 非存储: Stream 本身不存储任何数据。它像一个传送带,数据源的元素在其上流过,被加工处理。
  • 惰性求值: 这是 Stream 的核心性能优势。所有中间操作都不会立即执行,它们只是在构建处理配方。只有当终端操作被调用时,数据源的元素才开始真正地在流水线上流动和处理。

1.3.3. 常用 Stream 方法详解(关键补充)

在看综合示例前,我们先了解一些最核心的 Stream API 方法。

方法签名类型描述
stream()创建操作从集合(如 List, Set)获取一个 Stream。这是所有操作的入口。
filter(Predicate<T> p)中间操作过滤。接收一个返回 boolean 的 Lambda 表达式。只有当表达式为 true 时,元素才会被保留。
map(Function<T, R> f)中间操作映射/转换。将流中的每个元素转换为另一个元素。例如,从 User 对象中提取 String 类型的姓名。
sorted(Comparator<T> c)中间操作排序。根据提供的 Comparator 对流中的元素进行排序。无参版本 sorted() 按自然顺序排序。
collect(Collector<T,A,R> c)终端操作收集。将流中的元素收集到一个结果容器中,最常用的是 Collectors.toList()Collectors.toSet()
forEach(Consumer<T> action)终端操作遍历。对流中的每个元素执行指定的操作。常用于打印输出。

1.3.4. 实战演练:链式管道查询

现在,我们具备了所有必要的知识。让我们用 Stream API 重新实现 1.3.1 中的场景。

文件路径: src/main/java/com/example/notes/ch01/StreamExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.notes.ch01;

import com.example.notes.data.UserDataSource;
import com.example.notes.entity.Role;
import com.example.notes.entity.User;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
public static void main(String[] args) {
List<User> userList = UserDataSource.getUsers();

// 使用 Stream API 一气呵成
List<String> adminNames = userList.stream() // 1. 获取数据源
.filter(user -> Role.ADMIN.equals(user.getRole())) // 2. 过滤:只保留 ADMIN
.sorted(Comparator.comparingInt(User::getAge).reversed()) // 3. 排序:按年龄降序
.map(User::getName) // 4. 映射:提取用户姓名
.collect(Collectors.toList()); // 5. 收集:将结果汇集成 List

System.out.println("排序后的管理员姓名: " + adminNames);
}
}
// 输出:
// 排序后的管理员姓名: [Diana, Fiona, Alice]

1.4. 处理值缺失:Optional 类型

Optional<T> 是一个容器类,旨在通过类型系统来明确表达值缺失的可能性,从而在编译层面就促使开发者处理这种情况,以避免 NullPointerException

1.4.1. 背景:NullPointerException 的根源

危险: NullPointerException (NPE) 是 Java 中最常见的运行时异常。它通常发生在调用一个值为 null 的引用的方法或访问其属性时。返回 null 来表示“未找到”是一种脆弱的设计,因为它将检查责任完全推给了调用方。


1.4.2. 核心概念:Optional 容器详解

Optional 强制开发者必须“打开”容器才能获取值,在此过程中自然地处理了值缺失的可能性。

API描述
Optional.ofNullable(value)(最常用) 创建一个 Optional,value 可以为 null。
ifPresent(Consumer<T>)(推荐) 若值存在,则对其执行 Consumer 操作。
orElse(T other)若值存在则返回它,否则返回一个默认值 other
orElseGet(Supplier<T>)若值存在则返回它,否则通过 Supplier 生成一个默认值(懒加载)。
map(Function<T, R>)若值存在,则对其进行映射转换,返回一个新的 Optional<R>
orElseThrow()若值存在则返回它,否则抛出 NoSuchElementException

1.4.3. 实战:构建安全的查询方法

场景: 实现一个 findUserByName(String name) 方法,并安全地处理其返回结果。

文件路径: src/main/java/com/example/notes/ch01/OptionalExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.notes.ch01;

import cn.hutool.core.collection.CollUtil;
import com.example.notes.data.UserDataSource;
import com.example.notes.entity.Role;
import com.example.notes.entity.User;
import java.util.List;
import java.util.Optional;

public class OptionalExample {
public static Optional<User> findUserByName(List<User> users, String name) {
// 通过 Hutool 封装一个 findOne 方法,用于查找列表中的其中一个用户
return Optional.ofNullable(CollUtil.findOne(users, user -> user.getName().equalsIgnoreCase(name)));
}

public static void main(String[] args) {
List<User> userList = UserDataSource.getUsers();

// 场景1: 查找存在的用户 'Diana'
System.out.println("--- 查找 'Diana' ---");
Optional<User> dianaOpt = findUserByName(userList, "Diana");
dianaOpt.ifPresent(user -> System.out.println("找到了! 年龄: " + user.getAge()));

// 场景2: 查找不存在的用户 'Tom'
System.out.println("\n--- 查找 'Tom' ---");
User tom = findUserByName(userList, "Tom")
.orElse(new User(0L, "Guest", 0, Role.MEMBER, null));
System.out.println("查找结果: " + tom.getName());

// 场景3: 链式调用获取可能不存在的属性
Integer age = findUserByName(userList, "Bob")
.map(User::getAge)
.orElse(-1);
System.out.println("\nBob 的年龄是: " + age);
}
}
// 输出:
// --- 查找 'Diana' ---
// 找到了! 年龄: 42
//
// --- 查找 'Tom' ---
// 查找结果: Guest
//
// Bob 的年龄是: 35

第二章:代码现代化与领域建模

在掌握了函数式数据处理的基础后,本章我们将把目光投向 Java 10 及后续版本中一系列旨在提升代码简洁性可读性领域建模能力的语法增强。这些特性,如 RecordvarSealed ClassSwitch 模式匹配,共同构成了现代 Java 开发的基石,能帮助开发者编写出更安全、更具表达力的代码。


2.1. [简洁之道] 数据载体 Record 与类型推断 var

本节将聚焦于两个极大地提升了开发效率的“语法糖”:Record 类用于终结数据对象(POJO)的样板代码,var 关键字则用于简化局部变量的声明。

2.1.1. 背景:Lombok 虽好,但并非银弹

在第一章的准备工作中,我们使用了 Lombok 的 @Value 注解来快速创建一个不可变的 User 类。Lombok 在生产项目中是提升效率的利器,但它也有一些权衡:

  • “魔法”性: Lombok 通过注解处理器在编译期生成代码,这意味着开发者在源码(.java 文件)中看不到真实的 getterequals 等方法,有时会给调试和理解带来不便。
  • 环境依赖: 需要在 IDE 中安装对应的插件,否则代码会报错,增加了环境配置的复杂度。
  • 语言的演进: 社区对“减少样板代码”的强烈需求,最终推动了 Java 语言自身的发展,催生了原生的解决方案——Record

2.1.2. 核心概念:Record 详解

Record (JEP 395, Java 16 正式发布) 是一种特殊的、用于充当 不可变数据透明载体 的类。当一个类的主要目的就是传递数据时,Record 是最佳选择。

开发者只需声明数据组件,编译器就会自动生成以下所有成员:

生成的成员描述
私有 final 字段对应于声明中的每一个组件,确保了 不可变性
全参构造器一个公共的、包含了所有组件的构造器,被称为“规范构造器”。
公共访问器方法每个组件都有一个同名的访问器方法,如 user.age(),注意没有 get 前缀。
equals(Object o)基于所有组件的值进行比较的、完备的实现。
hashCode()基于所有组件的值计算出的、一致的哈希码。
toString()包含了类名和所有组件及其值的、清晰的字符串表示。

2.1.3. 实战:使用 Record 重构 User

现在,我们可以用 Record 来重构之前使用 Lombok @Value 定义的 User 类,体验语言原生解决方案的优雅。

文件路径: src/main/java/com/example/notes/entity/User.java (修改后)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.notes.entity;

import java.time.LocalDateTime;

/**
* 用户数据类 (使用 Record 重构)
* Record 提供了一种语言级的、用于创建不可变数据聚合的简洁语法。
*/
public record User(
long id,
String name,
int age,
Role role,
LocalDateTime registrationDate
) {}

对比: 仅仅一行 public record User(...) {} 就完全替代了之前使用 @Value 的类或手动编写的几十行 POJO 代码。代码不仅更短,而且其“数据载体”的意图也更加明确,同时无需任何第三方库或 IDE 插件。


2.1.4. 核心概念:局部变量类型推断 var

var (JEP 286, Java 10 正式发布) 关键字允许编译器根据变量初始化表达式来自动推断其类型,从而省略在左侧的显式类型声明。

重要: var 只是一个“语法糖”,它并没有改变 Java 是静态类型语言的本质。变量的类型在编译时就已经确定,并且之后不能再改变。它仅仅是为开发者省去了手动声明类型的麻烦。

使用规则

  • 必须初始化: var 声明的变量必须在同一条语句中进行初始化,如 var name = "Alice";
  • 仅限局部变量: var 只能用于方法内的局部变量、for 循环、try-with-resources 语句中。不能用于成员变量(字段)、方法参数或方法返回类型。
  • 不能用 null 初始化: var x = null; 是非法的,因为无法从 null 推断出具体类型。

2.1.5. 实战:使用 var 简化代码

在日常代码中,var 尤其能简化那些泛型类型复杂或类名冗长的变量声明。

文件路径: src/main/java/com/example/notes/ch02/VarExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.notes.ch02;

import com.example.notes.data.UserDataSource;
import com.example.notes.entity.Role;
import com.example.notes.entity.User;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class VarExample {
public static void main(String[] args) {
// 场景1: 简化普通变量声明
var userList = UserDataSource.getUsers();
System.out.println("userList 的类型是 List: " + (userList instanceof List));

// 场景2: 极大地简化复杂的泛型类型声明
// 传统写法: Map<Role, List<User>> usersByRole = ...
var usersByRole = userList.stream()
.collect(Collectors.groupingBy(User::role));

System.out.println("usersByRole 的类型是 Map: " + (usersByRole instanceof Map));

// 场景3: 在 for-each 循环中使用
System.out.println("\n管理员信息:");
for (var user : usersByRole.get(Role.ADMIN)) {
// 此处 user 的类型被正确推断为 User
System.out.println(" - " + user.name());
}
}
}
// 输出:
// userList 的类型是 List: true
// usersByRole 的类型是 Map: true
//
// 管理员信息:
// - Alice
// - Diana
// - Fiona

2.2. [精准之道] 可控继承:Sealed 类与接口

Sealed (JEP 409, Java 17 正式发布) 关键字为 Java 的继承体系带来了更强的控制力,它允许一个类或接口明确地声明“谁可以成为我的直接子类”。

2.2.1. 背景:无限继承的风险

在标准面向对象模型中,任何 public 的非 final 类都可以被项目中的任意其他类继承。但在某些业务场景下,这种“无限开放”的继承关系反而是一种风险。

场景: 假设一个支付系统需要处理不同的支付结果,这些结果只可能是“成功”、“失败”或“处理中”,不应该存在第四种未知的状态。如果 PaymentResult 是一个普通接口,任何人都可以在系统的任何地方创建一个新的、未经授权的实现,可能导致业务逻辑处理不完整。


2.2.2. 核心概念:Sealed 关键字详解

sealed 关键字通过与 permits 结合,将一个类型的继承体系变为封闭的。

  • sealed: 用于修饰类或接口,表示这是一个“密封”类型。
  • permits: 跟在 sealed 类型声明后,用于列出所有允许继承或实现的直接子类。

对子类的约束

所有在 permits 列表中指定的子类,都必须遵循以下三条规则之一:

  1. 必须声明为 `final`:表示继承关系到此为止,不能再被任何类继承。
  2. 必须声明为 `sealed`:表示它可以被继续继承,但同样需要用 permits 指定其下一级的子类。
  3. 必须声明为 `non-sealed`:表示“解除密封”,该子类回归到普通的开放继承模式,任何类都可以继承它。

2.2.3. 实战:构建封闭的支付结果体系

我们将使用 sealed 接口和 record 来共同构建一个类型安全、表达力强的支付结果领域模型。

文件路径: src/main/java/com/example/notes/entity/PaymentResult.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.notes.entity;

/**
* 定义一个密封接口 PaymentResult,
* 它只允许 Success, Failure, 和 Pending 这三个类来实现。
*/
public sealed interface PaymentResult permits PaymentResult.Success, PaymentResult.Failure, PaymentResult.Pending {

/**
* 成功状态:使用 record 定义,包含交易ID和金额
*/
record Success(String transactionId, long amount) implements PaymentResult {}

/**
* 失败状态:使用 record 定义,包含失败原因
*/
record Failure(String reason) implements PaymentResult {}

/**
* 处理中状态:使用 record 定义,包含一个提示信息
*/
record Pending(String message) implements PaymentResult {}
}

通过这种方式,我们从语言层面保证了 PaymentResult 的实例只可能是 Success, Failure, 或 Pending 三种之一,为后续进行详尽的逻辑处理奠定了坚实的基础。


2.3. [表达之道] 终极武器:Switch 模式匹配

Switch 模式匹配 (JEP 441, Java 21 正式发布) 是对 Java switch 语句的革命性增强,它允许 switch 对任意类型的对象进行匹配,并能结合 when 子句进行更复杂的条件判断。

2.3.1. 背景:instanceof 与类型转换的繁琐

switch 模式匹配出现之前,处理像上一节定义的 PaymentResult 这样的多态对象,通常需要依赖一长串的 if-else if-else 链,并在每个分支中进行 instanceof 类型检查和手动的强制类型转换。

1
2
3
4
5
6
7
8
9
10
11
// 传统方式处理支付结果
PaymentResult result = ...;
if (result instanceof PaymentResult.Success) {
PaymentResult.Success s = (PaymentResult.Success) result; // 手动强制转换
System.out.println("支付成功,交易号: " + s.transactionId());
} else if (result instanceof PaymentResult.Failure) {
PaymentResult.Failure f = (PaymentResult.Failure) result; // 手动强制转换
System.err.println("支付失败,原因: " + f.reason());
} else if (result instanceof PaymentResult.Pending) {
// ...
}

这种代码不仅冗长、易错,而且缺乏编译器级别的安全保障。


2.3.2. 核心概念:switch 模式匹配详解

增强后的 switch 可以作为表达式使用(即有返回值),并引入了强大的新功能。

新特性描述示例
类型模式case 标签可以直接匹配对象的类型,并将匹配到的对象绑定到一个新变量上,无需强制转换。case Success s -> ...
case nullswitch 可以直接处理 null 情况,无需在外部进行 if (obj == null) 判断。case null -> "结果为空"
守护模式 when在类型匹配的基础上,增加一个额外的布尔条件进行判断。case Success s when s.amount() > 1000 -> ...
穷尽性检查(安全保障) 当对 sealed 类型或枚举进行 switch 时,编译器会检查是否覆盖了所有可能的子类型,否则会报错。-

2.3.3. 实战:使用 switch 优雅地处理支付结果

switch 模式匹配与 sealed 接口的结合,是现代 Java 中构建和处理领域模型的最佳实践。

文件路径: src/main/java/com/example/notes/ch02/SwitchPatternExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.example.notes.ch02;

import com.example.notes.entity.PaymentResult;

public class SwitchPatternExample {

public static String handlePayment(PaymentResult result) {
// 使用 switch 表达式进行模式匹配
return switch (result) {
// case 1: 匹配 Success 类型,并将结果绑定到变量 s
case PaymentResult.Success s -> "支付成功! 交易号: " + s.transactionId() + ", 金额: " + s.amount();

// case 2: 匹配 Failure 类型,并将结果绑定到变量 f
case PaymentResult.Failure f -> "支付失败! 原因: " + f.reason();

// case 3: 匹配 Pending 类型,并将结果绑定到变量 p
case PaymentResult.Pending p -> "支付处理中... 信息: " + p.message();

// case 4: 直接处理 null 输入
case null -> "支付结果未知 (null)";

// 由于 PaymentResult 是 sealed 类型,且我们已覆盖所有子类和 null,
// 因此编译器知道这是“穷尽的”,无需 default 分支。
};
}

public static void main(String[] args) {
PaymentResult r1 = new PaymentResult.Success("TXN12345", 500L);
PaymentResult r2 = new PaymentResult.Failure("余额不足");
PaymentResult r3 = new PaymentResult.Pending("银行确认中");
PaymentResult r4 = null;

System.out.println(handlePayment(r1));
System.out.println(handlePayment(r2));
System.out.println(handlePayment(r3));
System.out.println(handlePayment(r4));
}
}
// 输出:
// 支付成功! 交易号: TXN12345, 金额: 500
// 支付失败! 原因: 余额不足
// 支付处理中... 信息: 银行确认中
// 支付结果未知 (null)

第3章:核心 API 的持续演进

在经历了前两章函数式编程与语法现代化这两场“大革命”后,本章我们将回归到 Java 平台自身的基础设施。我们将看到,Java 在后续的版本中,也从未停止过对那些我们日常使用最频繁的核心 API(如 Interface, String, Collections, Files 等)进行打磨与增强。这些“小而美”的改进,同样是提升开发体验和代码质量的关键。


3.1. [演进之美] 接口的革命:defaultstatic 方法

在 Java 8 之前,接口(Interface)是一个“纯粹”的契约,只能包含抽象方法和常量。这种设计在扩展已发布的接口时显得异常“脆弱”。Java 8 通过引入 defaultstatic 方法,赋予了接口全新的生命力。

3.1.1. 背景:接口的“脆弱性”

场景: 假设我们有一个 UserService 接口,它已经被项目内外的数十个类所实现。现在,我们需要为这个接口增加一个新功能,例如 banUser(User user)

在 Java 8 之前,这是一个灾难性的操作。一旦我们在 UserService 接口中添加新的抽象方法 void banUser(User user);,所有已经实现该接口的类都会立刻出现编译错误,因为它们没有实现这个新方法。这使得对已发布 API 的演进几乎寸步难行。


3.1.2. 核心概念:default 方法

default 方法允许我们在接口中为方法提供一个默认实现。当一个类实现该接口时,它会自动继承这个默认方法,而无需强制去实现它

  • 核心优势: 实现了 API 的向后兼容。库的作者可以放心地为接口添加新功能,而不会破坏已有用户的代码。
  • 重写: 实现类如果对默认实现不满意,可以选择性地 @Override 这个 default 方法,提供自己的实现。
  • 多重继承冲突: 如果一个类实现了多个接口,且这些接口包含签名相同的 default 方法,Java 会强制该类必须重写此方法,以明确指定使用哪个实现,或提供全新的实现,从而避免了“菱形问题”。

3.1.3. 实战:为 UserService 接口添加默认方法

我们将定义一个 UserService 接口,并为其添加一个依赖于抽象方法的 default 方法,这是一种常见的设计模式。

文件路径: src/main/java/com/example/notes/service/UserService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.example.notes.service;

import com.example.notes.entity.Role;
import com.example.notes.entity.User;
import java.util.Optional;

public interface UserService {

/**
* 这是一个抽象方法,任何实现类都必须提供自己的实现。
* @param id 用户ID
* @return 一个包含用户的 Optional,如果找不到则为空
*/
Optional<User> findById(long id);

/**
* 这是一个默认方法,它依赖于上面的抽象方法 findById。
* 它为所有实现类提供了一个开箱即用的、判断用户是否为管理员的功能。
* @param id 用户ID
* @return 如果是管理员则返回 true, 否则返回 false
*/
default boolean isUserAdmin(long id) {
// 直接调用接口内的抽象方法
return findById(id)
.map(user -> user.role() == Role.ADMIN) // 如果用户存在,检查其角色
.orElse(false); // 如果用户不存在,默认为 false
}
}

3.1.4. 核心概念:static 方法

Java 8 还允许在接口中定义 static 方法。这些方法与接口的任何实例都无关,它们直接属于接口本身。

  • 核心用途: 用于存放与该接口紧密相关的工具类方法。这避免了为了几个辅助方法而创建一个全新的 XxxUtils 工具类的尴尬(例如,在 Collection 接口出现之前,我们只能使用 Collections 这个工具类)。

3.1.5. 实战:为 UserService 接口添加静态工厂方法

在接口中提供一个静态的工厂方法,用于创建该接口的实例,是一种非常现代和优雅的设计。

文件路径: src/main/java/com/example/notes/service/InMemoryUserService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.notes.service;

import com.example.notes.data.UserDataSource;
import com.example.notes.entity.User;
import java.util.List;
import java.util.Optional;

/**
* UserService 的一个基于内存的实现类
*/
public class InMemoryUserService implements UserService {
private final List<User> users = UserDataSource.getUsers();

@Override
public Optional<User> findById(long id) {
return users.stream()
.filter(user -> user.id() == id)
.findFirst();
}
}

文件路径: src/main/java/com/example/notes/ch03/InterfaceExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.example.notes.ch03;

import com.example.notes.service.InMemoryUserService;
import com.example.notes.service.UserService;

public class InterfaceExample {

// 在 UserService 接口中添加一个静态工厂方法
public interface UserService {
// ... (原有的 findById 和 default isUserAdmin 方法)

/**
* 静态工厂方法,用于创建 UserService 的一个内存实现实例。
* 调用方无需关心具体的实现类是哪个。
* @return UserService 的一个实例
*/
static UserService createInMemoryInstance() {
return new InMemoryUserService();
}
}

public static void main(String[] args) {
// 通过接口的静态方法直接获取服务实例
UserService service = UserService.createInMemoryInstance();

// 调用实例方法
System.out.println("查找ID为1的用户: " + service.findById(1L).orElse(null));

// 调用从接口继承的 default 方法,无需在实现类中编写任何代码
System.out.println("ID为1的用户是管理员吗? " + service.isUserAdmin(1L));
System.out.println("ID为2的用户是管理员吗? " + service.isUserAdmin(2L));
}
}
// 输出:
// 查找ID为1的用户: User[id=1, name=Alice, age=28, role=ADMIN, registrationDate=2023-09-15T00:00]
// ID为1的用户是管理员吗? true
// ID为2的用户是管理员吗? false

3.2. [便利之源] 核心 API 增强

除了接口的革命性变化,Java 在后续版本中,也持续对我们日常使用的核心类(如 String, Files)和集合的创建方式进行了大量优化。

3.2.1. 集合工厂方法 (Java 9)

背景: 在 Java 9 之前,创建一个包含少量元素的列表或集合,代码比较繁琐,且 Arrays.asList() 创建的列表并非真正的不可变。

核心概念: Java 9 引入了一系列静态工厂方法 List.of(), Set.of(), Map.of(),用于一步创建不可变的集合。

特性:

  1. 不可变: 任何尝试对其进行添加、删除等修改操作,都会抛出 UnsupportedOperationException
  2. Nulls Forbidden: 不允许存入 null 元素或键/值。
  3. 高效紧凑: 内部实现经过优化,占用内存更少。

实战演练
文件路径: src/main/java/com/example/notes/ch03/CollectionFactoryExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.notes.ch03;

import java.util.List;
import java.util.Map;
import java.util.Set;

public class CollectionFactoryExample {
public static void main(String[] args) {
// 创建不可变 List
List<String> names = List.of("Alice", "Bob", "Charlie");
System.out.println("Immutable List: " + names);

// 创建不可变 Set
Set<Integer> numbers = Set.of(10, 20, 30);
System.out.println("Immutable Set: " + numbers);

// 创建不可变 Map
Map<String, String> config = Map.of("version", "v1.2", "env", "prod");
System.out.println("Immutable Map: " + config);

// 尝试修改会抛出异常
try {
names.add("David");
} catch (UnsupportedOperationException e) {
System.out.println("\n尝试修改 List... 捕获到异常: " + e.getClass().getSimpleName());
}
}
}
// 输出:
// Immutable List: [Alice, Bob, Charlie]
// Immutable Set: [30, 20, 10]
// Immutable Map: {env=prod, version=v1.2}
//
// 尝试修改 List... 捕获到异常: UnsupportedOperationException

3.2.2. String API 增强与文本块 (Java 11+)

核心概念: Java 11 引入了多个实用的 String 方法。Java 15 则正式引入了文本块,极大地改善了多行字符串的书写体验。

方法 / 特性引入版本描述
isBlank()Java 11判断字符串是否为空白(isEmpty() 或只包含 Unicode 空白字符)。
lines()Java 11将字符串按行分隔符拆分为一个 Stream<String>,非常适合逐行处理。
strip()Java 11去除字符串首尾的空白字符(比 trim() 更智能,能识别 Unicode 空白符)。
repeat(int)Java 11将字符串重复指定次数。
文本块Java 15使用 """...""" 定义多行字符串,无需手动添加 \n+ 连接符。

实战演练
文件路径: src/main/java/com/example/notes/ch03/StringExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.example.notes.ch03;

import java.util.stream.Collectors;

public class StringExample {
public static void main(String[] args) {
// 1. isBlank()
System.out.println("' '.isBlank() = " + " ".isBlank()); // true
System.out.println("''.isBlank() = " + "".isBlank()); // true

// 2. lines() 和 strip()
String multiline = " \n line 1 \n line 2 \n ";
List<String> lines = multiline.lines()
.filter(line -> !line.isBlank())
.map(String::strip)
.collect(Collectors.toList());
System.out.println("处理后的行: " + lines);

// 3. 文本块
String welcomeEmail = """
Hello, {userName}!
Thank you for registering.
Your account is now active.

Best Regards,
The Team
""";
System.out.println("\n--- 邮件模板 ---");
System.out.println(welcomeEmail.replace("{userName}", "Alice"));
}
}
// 输出:
// ' '.isBlank() = true
// ''.isBlank() = true
// 处理后的行: [line 1, line 2]
//
// --- 邮件模板 ---
// Hello, Alice!
// Thank you for registering.
// Your account is now active.
//
// Best Regards,
// The Team

3.2.3. Files API 增强 (Java 11)

背景: 在 Java 11 之前,读写小文件也需要编写 try-with-resourcesBufferedReader/Writer 等样板代码。

核心概念: Java 11 在 java.nio.file.Files 类中添加了两个非常便捷的静态方法,极大简化了小文件的读写操作。

  • Files.writeString(Path path, CharSequence csq): 将一个字符串直接写入文件。
  • Files.readString(Path path): 将整个文件内容直接读成一个字符串。

实战演练
文件路径: src/main/java/com/example/notes/ch03/FilesExample.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.notes.ch03;

import cn.hutool.json.JSONUtil;
import com.example.notes.entity.Role;
import com.example.notes.entity.User;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;

public class FilesExample {
public static void main(String[] args) throws IOException {
User user = new User(100L, "TempUser", 99, Role.MEMBER, LocalDateTime.now());

// 1. 定义文件路径
Path userFile = Path.of("user_data.json");
System.out.println("文件将保存在: " + userFile.toAbsolutePath());

// 2. 将 User 对象转为 JSON 字符串 (使用 Hutool)
String userJson = JSONUtil.toJsonPrettyStr(user);

// 3. 使用 Files.writeString() 一行代码写入文件
Files.writeString(userFile, userJson);
System.out.println("\n用户信息已写入文件。");

// 4. 使用 Files.readString() 一行代码读取文件
String readJson = Files.readString(userFile);
System.out.println("\n从文件中读回的内容:");
System.out.println(readJson);

// 5. 将 JSON 字符串转回 User 对象 (使用 Hutool)
User userReadFromFile = JSONUtil.toBean(readJson, User.class);
System.out.println("\n从 JSON 解析回的对象: " + userReadFromFile);

// 6. 清理临时文件
Files.delete(userFile);
System.out.println("\n临时文件已删除。");
}
}