跳到主内容
创建时间: 2026-09-16最后更新: 2026-09-16

接口可以把调用方与具体实现分开,但接口的方法仍然需要准确表达输入和返回值。后面的 Repository 会保存多条 HealthStatus,按服务名称查找一条记录,并返回分页结果,这些能力离不开集合与泛型。

本章继续使用普通 Java 对象,依次理解 List、Map、泛型和 Optional。最后再学习最常见的 Stream 转换,让后续数据访问代码中的方法签名不再只是需要记忆的写法。

1. 多个对象

上一章中的变量一次只引用一个对象:

single-status.java
1
HealthStatus status = new HealthStatus("UP", "spring");

当程序需要保存多次检查结果时,可以使用数组:

status-array.java
1
HealthStatus[] statuses = {
2
new HealthStatus("UP", "spring"),
3
new HealthStatus("UP", "postgresql")
4
};

数组的长度在创建后不能改变。应用数据通常会持续增加、删除或查询,因此 Java 集合框架提供了更适合动态数据的接口与实现。

当前项目会遇到三种典型需求:

需求合适的类型原因
按顺序返回多条状态List<HealthStatus>保留元素顺序,可以按索引访问
按服务名称保存最新状态Map<String, HealthStatus>使用服务名称作为键快速查找
查询结果可能不存在Optional<HealthStatus>在返回类型中明确表达缺失

List 和 Map 是接口,ArrayList、HashMap、ConcurrentHashMap 等类提供不同实现。变量通常声明为接口类型,让调用代码只依赖需要的集合能力。

2. List

List<E> 表示有顺序、允许重复的元素序列,其中 E 是元素类型。创建一份不可修改的示例列表:

status-list.java
1
List<HealthStatus> statuses = List.of(
2
new HealthStatus("UP", "spring"),
3
new HealthStatus("DOWN", "postgresql")
4
);
5
6
HealthStatus first = statuses.get(0);
7
int count = statuses.size();

代码需要引入 java.util.List。索引从 0 开始,因此 get(0) 取得第一项。访问小于 0 或大于等于 size() 的位置会抛出 IndexOutOfBoundsException。

List.of() 创建的列表不能增加、删除或替换元素。需要在内存中修改列表时,可以使用 ArrayList:

mutable-list.java
1
List<HealthStatus> statuses = new ArrayList<>();
2
statuses.add(new HealthStatus("UP", "spring"));
3
statuses.add(new HealthStatus("UP", "postgresql"));
4
statuses.remove(0);

这段代码还要引入 java.util.ArrayList。左侧仍然声明为 List<HealthStatus>,右侧才选择具体实现,调用方因此不会依赖 ArrayList 特有细节。

可以使用增强 for 循环依次读取元素:

iterate-list.java
1
for (HealthStatus status : statuses) {
2
System.out.println(status.service());
3
}

集合自身可变与集合中对象可变是两件事。List.of() 不允许替换元素,但如果元素本身是可变对象,它的字段仍可能改变。当前 HealthStatus 使用 record,因此对象创建后状态不会被重新赋值。

3. Map

Map<K, V> 保存键和值之间的对应关系。当前项目希望每个服务只保留一条最新状态,因此可以让服务名称成为键:

status-map.java
1
Map<String, HealthStatus> statuses = new HashMap<>();
2
3
statuses.put("spring", new HealthStatus("UP", "spring"));
4
statuses.put("postgresql", new HealthStatus("UP", "postgresql"));
5
6
HealthStatus springStatus = statuses.get("spring");

代码需要引入 java.util.Map 和 java.util.HashMap。put() 使用相同键时会替换旧值,所以这份 Map 天然表达「每个服务的最新状态」,而不是完整历史记录。

get() 返回 null 可能表示键不存在,也可能表示键存在但值就是 null。当前项目不保存 null,但 Repository 的公开查询仍会使用 Optional 明确告诉调用方结果可能缺失,而不是把 null 的含义留给调用方猜测。

普通 HashMap 不能保证多个线程同时读写时仍然正确。Spring Web 应用可能并发处理请求,后面的内存 Repository 会使用 ConcurrentHashMap。它仍然实现 Map 接口,但提供适合并发访问的实现策略。

需要返回 Map 中的全部状态时,可以取得值集合,再复制成列表:

map-values.java
1
List<HealthStatus> allStatuses = List.copyOf(statuses.values());

复制后的列表不会随着原 Map 的后续修改而变化,也不会把 Map 的可修改视图直接暴露给调用方。

4. 泛型

尖括号中的类型就是泛型参数。List<HealthStatus> 表示这个列表只接受 HealthStatus,Map<String, HealthStatus> 则表示键是 String、值是 HealthStatus。

泛型让错误在编译阶段暴露:

generic-check.java
1
List<HealthStatus> statuses = new ArrayList<>();
2
statuses.add(new HealthStatus("UP", "spring"));
3
// statuses.add("UP"); // 编译失败:String 不是 HealthStatus

读取元素时也不需要手动把 Object 转回具体类型。编译器已经知道 statuses.get(0) 的结果是 HealthStatus。

类和 record 也可以声明自己的类型参数。分页结果不需要固定只能保存心跳记录:

src/main/java/cn/usehook/spring/health/PageResult.java
01
package cn.usehook.spring.health;
02
03
import java.util.List;
04
05
public record PageResult<T>(
06
List<T> items,
07
int page,
08
int size,
09
long totalElements
10
) {}

T 是由使用方决定的占位类型:

page-result.java
1
PageResult<HealthStatus> result = new PageResult<>(
2
statuses,
3
0,
4
20,
5
statuses.size()
6
);

创建对象时,T 被确定为 HealthStatus,所以 items() 的返回类型就是 List<HealthStatus>。菱形语法 <> 允许编译器从左侧和构造参数推断重复的类型信息。

5. Optional

按服务名称查询时,数据库或 Map 中可能没有对应记录。Optional<T> 用一个对象表达「存在一个 T」或「没有结果」:

optional-result.java
1
HealthStatus status = statuses.get("redis");
2
Optional<HealthStatus> result = Optional.ofNullable(status);

代码需要引入 java.util.Optional。ofNullable() 在参数为 null 时返回空 Optional,否则包装该对象。已经确定非空时可以使用 Optional.of(),明确没有结果时可以使用 Optional.empty()。

调用方可以转换存在的结果,并为缺失提供默认值:

optional-use.java
1
String service = result
2
.map(HealthStatus::service)
3
.orElse("unknown");

map() 只在值存在时执行转换,空 Optional 会继续保持为空;orElse() 再提供回退值。后面 Repository 会返回 Optional<HealthStatus>,Service 根据具体用例决定缺失时返回默认值、继续保持为空,还是抛出业务异常。

不要把 Optional 用作所有字段的通用包装,也不要调用 get() 后期待值一定存在。它最适合用作可能没有单个查询结果的方法返回值;集合查询没有结果时直接返回空列表,通常比 Optional<List<T>> 更清楚。

6. Stream

集合可以通过 Stream 描述一连串读取与转换操作。理解 Stream 以前,需要先认识函数式接口和 Lambda。

函数式接口是只有一个抽象方法的接口。Java 标准库已经提供了常见类型:

函数式接口抽象方法的输入与输出常见用途
Predicate<T>接收 T,返回 boolean判断元素是否满足条件
Function<T, R>接收 T,返回 R把一种值转换为另一种值
Consumer<T>接收 T,没有返回值消费元素,例如输出或写入
Supplier<T>没有参数,返回 T延迟提供一个值

Lambda 是函数式接口的一种紧凑实现方式。它没有脱离类型系统独立存在,必须由赋值位置、方法参数或返回类型提供目标函数式接口:

lambda-types.java
1
Predicate<HealthStatus> healthy = status -> "UP".equals(status.status());
2
Function<HealthStatus, String> serviceName = status -> status.service();
3
4
boolean springIsHealthy = healthy.test(new HealthStatus("UP", "spring"));
5
String name = serviceName.apply(new HealthStatus("UP", "postgresql"));

这段代码需要引入 java.util.function.Predicate 和 java.util.function.Function。箭头左侧是参数,右侧是表达式或代码块。表达式结果会成为返回值;需要多条语句时使用代码块,并显式编写 return:

lambda-block.java
1
Function<HealthStatus, String> description = status -> {
2
String normalized = status.service().trim();
3
return status.status() + ":" + normalized;
4
};

Lambda 可以读取外部局部变量,但该变量必须是 final 或事实不可变,也就是赋值以后不再改变:

lambda-capture.java
1
String requiredStatus = "UP";
2
Predicate<HealthStatus> matches = status -> requiredStatus.equals(status.status());

这个限制避免同一段 Lambda 在稍后执行时面对不断变化的局部变量。对象字段和集合本身仍可能可变,所以「可以捕获引用」不表示并发读写自动安全。

当 Lambda 只是把参数原样交给一个已有方法时,可以使用方法引用。HealthStatus::service 等价于 status -> status.service();System.out::println 则等价于 value -> System.out.println(value)。方法引用没有改变执行逻辑,只减少重复的参数转发。

现在从所有状态中筛选可用服务,再只保留服务名称:

status-stream.java
1
List<String> availableServices = statuses.stream()
2
.filter(status -> "UP".equals(status.status()))
3
.map(HealthStatus::service)
4
.toList();

stream() 创建处理序列,filter() 保留满足条件的元素,map() 把元素转换成另一种值,toList() 才收集最终结果。

filter() 的参数类型是 Predicate<HealthStatus>,因此编译器知道 status -> ... 必须返回布尔值。map() 需要把 HealthStatus 转换为另一种类型,方法引用返回的 String 使后续 Stream 成为 Stream<String>。

Stream 的中间操作通常是惰性的。只调用 filter() 和 map() 时只是描述处理步骤,直到 toList()、forEach()、count() 等终止操作出现,元素才真正流过处理链。一个 Stream 终止后不能再次使用;需要重新处理时,应从集合创建新的 Stream。

Stream 不会自动让查询更高效。对内存列表调用 stream(),筛选仍然发生在 Java 进程中;数据库数据应尽量通过 SQL 条件筛选后再返回。后面的分页文章会继续保持这个边界。

现在已经可以读懂 Repository 中的 List<HealthStatus>、Map<String, HealthStatus> 和 Optional<HealthStatus>,也能理解分页结果为什么使用泛型。下一篇补上 Java 异常传播,解释方法失败时怎样中断正常返回路径;随后再学习注解与反射,最后进入 IoC 容器与 Spring Bean。