第一章:函数式数据处理 本章将系统性地介绍 Java 8 为开发者带来的 函数式编程范式 ,其核心是 Lambda 表达式 、Stream API 与 Optional 类型 。学习本章,开发者将能够理解并掌握一套全新的、更高效的数据处理思想与工具,目标是将传统冗长、繁琐的 指令式代码 ,重构为现代化、简洁、且高度可读的 声明式代码 。
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;@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) { 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 (User u1, User u2) -> { return u1.getAge() - u2.getAge(); } (u1, u2) -> { return u1.getAge() - u2.getAge(); } (u1, u2) -> u1.getAge() - u2.getAge() 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 按年龄排序后:" ); 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()); Predicate<User> isAdmin = user -> Role.ADMIN.equals(user.getRole()); System.out.println("Alice 是管理员吗? " + isAdmin.test(testUser)); Function<User, String> getName = user -> user.getName(); System.out.println("用户的姓名是: " + getName.apply(testUser)); Consumer<User> printUser = user -> System.out.println("正在消费用户: " + user); printUser.accept(testUser); Supplier<User> guestFactory = () -> new User (0L , "Guest" , 0 , Role.MEMBER, LocalDateTime.now()); System.out.println("创建的游客用户: " + guestFactory.get()); } }
1.2.5. 语法升华:方法引用 当 Lambda 表达式的方法体恰好是调用一个已存在的方法时,可以使用 方法引用 (::
) 来进一步简化代码。
方法引用的四种类型
类型 语法示例 等价 Lambda 静态方法引用 Integer::parseInt
s -> Integer.parseInt(s)
实例方法引用 (特定类型) String::toUpperCase
s -> 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 按年龄排序后:" ); 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),它由三个核心部分组成:
数据源 : 流水线的起点。可以是集合、数组、I/O 通道等。通过调用 .stream()
方法从一个集合获取流。中间操作 : 对数据进行处理的环节,如筛选、排序、转换。每个中间操作都会返回一个新的 Stream ,这使得操作可以链接起来。终端操作 : 流水线的终点。它会触发整个流水线的计算并产生最终结果,如生成一个 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(); List<String> adminNames = userList.stream() .filter(user -> Role.ADMIN.equals(user.getRole())) .sorted(Comparator.comparingInt(User::getAge).reversed()) .map(User::getName) .collect(Collectors.toList()); System.out.println("排序后的管理员姓名: " + adminNames); } }
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) { return Optional.ofNullable(CollUtil.findOne(users, user -> user.getName().equalsIgnoreCase(name))); } public static void main (String[] args) { List<User> userList = UserDataSource.getUsers(); System.out.println("--- 查找 'Diana' ---" ); Optional<User> dianaOpt = findUserByName(userList, "Diana" ); dianaOpt.ifPresent(user -> System.out.println("找到了! 年龄: " + user.getAge())); System.out.println("\n--- 查找 'Tom' ---" ); User tom = findUserByName(userList, "Tom" ) .orElse(new User (0L , "Guest" , 0 , Role.MEMBER, null )); System.out.println("查找结果: " + tom.getName()); Integer age = findUserByName(userList, "Bob" ) .map(User::getAge) .orElse(-1 ); System.out.println("\nBob 的年龄是: " + age); } }
第二章:代码现代化与领域建模 在掌握了函数式数据处理的基础后,本章我们将把目光投向 Java 10 及后续版本中一系列旨在提升代码简洁性 、可读性 与领域建模能力 的语法增强。这些特性,如 Record
、var
、Sealed Class
和 Switch 模式匹配
,共同构成了现代 Java 开发的基石,能帮助开发者编写出更安全、更具表达力的代码。
2.1. [简洁之道] 数据载体 Record
与类型推断 var
本节将聚焦于两个极大地提升了开发效率的“语法糖”:Record
类用于终结数据对象(POJO)的样板代码,var
关键字则用于简化局部变量的声明。
2.1.1. 背景:Lombok 虽好,但并非银弹 在第一章的准备工作中,我们使用了 Lombok 的 @Value
注解来快速创建一个不可变的 User
类。Lombok 在生产项目中是提升效率的利器,但它也有一些权衡:
“魔法”性 : Lombok 通过注解处理器在编译期生成代码,这意味着开发者在源码(.java
文件)中看不到真实的 getter
、equals
等方法,有时会给调试和理解带来不便。环境依赖 : 需要在 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;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) { var userList = UserDataSource.getUsers(); System.out.println("userList 的类型是 List: " + (userList instanceof List)); var usersByRole = userList.stream() .collect(Collectors.groupingBy(User::role)); System.out.println("usersByRole 的类型是 Map: " + (usersByRole instanceof Map)); System.out.println("\n管理员信息:" ); for (var user : usersByRole.get(Role.ADMIN)) { System.out.println(" - " + user.name()); } } }
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
列表中指定的子类,都必须遵循以下三条规则之一:
必须声明为 `final` :表示继承关系到此为止,不能再被任何类继承。 必须声明为 `sealed` :表示它可以被继续继承,但同样需要用 permits
指定其下一级的子类。 必须声明为 `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;public sealed interface PaymentResult permits PaymentResult.Success, PaymentResult.Failure, PaymentResult.Pending { record Success (String transactionId, long amount) implements PaymentResult {} record Failure (String reason) implements PaymentResult {} 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 null
switch
可以直接处理 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) { return switch (result) { case PaymentResult.Success s -> "支付成功! 交易号: " + s.transactionId() + ", 金额: " + s.amount(); case PaymentResult.Failure f -> "支付失败! 原因: " + f.reason(); case PaymentResult.Pending p -> "支付处理中... 信息: " + p.message(); case null -> "支付结果未知 (null)" ; }; } 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)); } }
第3章:核心 API 的持续演进 在经历了前两章函数式编程与语法现代化这两场“大革命”后,本章我们将回归到 Java 平台自身的基础设施。我们将看到,Java 在后续的版本中,也从未停止过对那些我们日常使用最频繁的核心 API(如 Interface
, String
, Collections
, Files
等)进行打磨与增强。这些“小而美”的改进,同样是提升开发体验和代码质量的关键。
3.1. [演进之美] 接口的革命:default
与 static
方法 在 Java 8 之前,接口(Interface
)是一个“纯粹”的契约,只能包含抽象方法和常量。这种设计在扩展已发布的接口时显得异常“脆弱”。Java 8 通过引入 default
和 static
方法,赋予了接口全新的生命力。
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 { Optional<User> findById (long id) ; default boolean isUserAdmin (long id) { return findById(id) .map(user -> user.role() == Role.ADMIN) .orElse(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;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 { public interface 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 )); System.out.println("ID为1的用户是管理员吗? " + service.isUserAdmin(1L )); System.out.println("ID为2的用户是管理员吗? " + service.isUserAdmin(2L )); } }
3.2. [便利之源] 核心 API 增强 除了接口的革命性变化,Java 在后续版本中,也持续对我们日常使用的核心类(如 String
, Files
)和集合的创建方式进行了大量优化。
3.2.1. 集合工厂方法 (Java 9) 背景 : 在 Java 9 之前,创建一个包含少量元素的列表或集合,代码比较繁琐,且 Arrays.asList()
创建的列表并非真正的不可变。
核心概念 : Java 9 引入了一系列静态工厂方法 List.of()
, Set.of()
, Map.of()
,用于一步创建不可变 的集合。
特性 :
不可变 : 任何尝试对其进行添加、删除等修改操作,都会抛出 UnsupportedOperationException
。Nulls Forbidden : 不允许存入 null
元素或键/值。高效紧凑 : 内部实现经过优化,占用内存更少。实战演练 文件路径 : 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<String> names = List.of("Alice" , "Bob" , "Charlie" ); System.out.println("Immutable List: " + names); Set<Integer> numbers = Set.of(10 , 20 , 30 ); System.out.println("Immutable Set: " + numbers); 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()); } } }
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) { System.out.println("' '.isBlank() = " + " " .isBlank()); System.out.println("''.isBlank() = " + "" .isBlank()); 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); 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" )); } }
3.2.3. Files API 增强 (Java 11) 背景 : 在 Java 11 之前,读写小文件也需要编写 try-with-resources
和 BufferedReader/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()); Path userFile = Path.of("user_data.json" ); System.out.println("文件将保存在: " + userFile.toAbsolutePath()); String userJson = JSONUtil.toJsonPrettyStr(user); Files.writeString(userFile, userJson); System.out.println("\n用户信息已写入文件。" ); String readJson = Files.readString(userFile); System.out.println("\n从文件中读回的内容:" ); System.out.println(readJson); User userReadFromFile = JSONUtil.toBean(readJson, User.class); System.out.println("\n从 JSON 解析回的对象: " + userReadFromFile); Files.delete(userFile); System.out.println("\n临时文件已删除。" ); } }