Java 树结构转换算法的再次优化:拥抱标准函数式接口

Java 树结构转换算法的再次优化:拥抱标准函数式接口

在前面的文章中,我们通过引入函数式接口成功解决了树结构转换算法的耦合性问题。但是,仔细观察会发现,我们定义的三个函数式接口实际上与 Java 8 标准函数式接口非常相似。本文将进一步优化实现,直接使用 Java 8 的标准函数式接口,让代码更加简洁和标准化。

从自定义接口到标准接口

图:左侧是优化后的实现,右侧为之前的实现

让我们来对比一下自定义接口与标准接口的映射关系:

1
2
3
4
// 自定义接口 -> Java 8 标准接口
GetParentId<T, R> -> Function<R, T>
GetId<T, R> -> Function<R, T>
AddChild<R, C> -> BiConsumer<R, R>

这种映射是完全自然的:

  • GetParentIdGetId 都是接收一个参数并返回一个值,正好对应 Function<T, R>
  • AddChild 接收两个参数且无返回值,正好对应 BiConsumer<T, U>

Tree 类的优化实现

使用标准函数式接口重构后的 Tree 类:

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
public class Tree {
/**
* 构建树形结构。
*
* @param nodes 节点集合
* @param getParentId 获取父节点ID的函数
* @param getId 获取节点ID的函数
* @param addChild 添加子节点的函数
* @param <T> 父节点ID类型
* @param <R> 节点类型
* @return 树形结构的根节点列表
*/
public static <T, R> List<R> buildTree(
Collection<R> nodes,
Function<R, T> getParentId,
Function<R, T> getId,
BiConsumer<R, R> addChild
) {
// 输入验证
if (nodes == null) {
return new ArrayList<>();
}
// nodes 转换为 id, node 的 Map
Map<T, R> nodeMap = nodes.stream()
.collect(Collectors.toMap(n -> getId.apply(n), n -> n));
for (R node : nodes) {
T parentId = getParentId.apply(node);
if (parentId == null) {
continue;
}
R parent = nodeMap.get(parentId);
if (parent != null) {
addChild.accept(parent, node);
} else {
// 处理找不到父节点的情况,这里可以记录日志
System.err.println("Parent node with id " + parentId
+ " not found for node " + getId.apply(node));
}
}
return nodes.stream()
.filter(n -> getParentId.apply(n) == null)
.collect(Collectors.toList());
}
}

优化的关键变化:

  1. 移除自定义函数式接口:不再需要定义 GetParentIdGetIdAddChild
  2. 使用标准接口:直接使用 Function<R, T>BiConsumer<R, R>
  3. 调用方法调整:从 .getId() 改为 .apply(),从 .addChild() 改为 .accept()

鸭子类型:行为一致性的完美体现

在编程世界中,有一个著名的概念叫做”鸭子类型”(Duck Typing),它来自于这样一句话:

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

虽然 Java 是静态类型语言,但在函数式编程的语境下,我们同样可以看到这种行为一致性的体现。TreeNewBeeTree 就是一个完美的例子。

接口形式不同,行为完全一致

让我们来对比这两个类的方法签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TreeNewBee 的方法签名
public static <T, R> List<R> buildTree(
Collection<R> nodes,
GetParentId<T, R> getParentId, // 自定义接口
GetId<T, R> getId, // 自定义接口
AddChild<R, R> addChild // 自定义接口
)

// Tree 的方法签名
public static <T, R> List<R> buildTree(
Collection<R> nodes,
Function<R, T> getParentId, // 标准函数式接口
Function<R, T> getId, // 标准函数式接口
BiConsumer<R, R> addChild // 标准函数式接口
)

完全兼容的使用方式

神奇的是,在我们的单元测试中,可以将 TreeNewBee 完全替换为 Tree,而调用代码无需任何修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 原来使用 TreeNewBee
List<Node> tree = TreeNewBee.buildTree(
nodes,
Node::getParentId, // 方法引用
Node::getId, // 方法引用
Node::addChild // 方法引用
);

// 直接替换为 Tree,完全兼容
List<Node> tree = Tree.buildTree(
nodes,
Node::getParentId, // 同样的方法引用
Node::getId, // 同样的方法引用
Node::addChild // 同样的方法引用
);

为什么能够完全兼容?

这种兼容性的根本原因在于:

  1. 行为契约一致:两个接口虽然定义不同,但描述的是相同的行为契约

    • 获取父节点ID:接收一个节点,返回其父节点ID
    • 获取节点ID:接收一个节点,返回其ID
    • 添加子节点:接收父节点和子节点,执行添加操作
  2. 方法引用的兼容性:Java 的方法引用具有强大的适配能力

    1
    2
    3
    // Node::getParentId 可以适配为:
    GetParentId<Integer, Node> getParentId = Node::getParentId; // 自定义接口
    Function<Node, Integer> getParentId = Node::getParentId; // 标准接口
  3. 编译器的智能推断:编译器根据上下文自动推断出正确的函数式接口类型

实际验证

在我们的测试代码中,无论是简单的 Node 对象还是复杂的 Map 类型,都表现出完美的兼容性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Map 类型测试 - TreeNewBee
List<Map<String, Object>> tree1 = TreeNewBee.buildTree(mapNodes,
map -> (Long) map.get("parentId"),
map -> (Long) map.get("id"),
(parent, child) -> {
@SuppressWarnings("unchecked")
List<Map<String, Object>> children =
(List<Map<String, Object>>) parent.get("children");
children.add(child);
});

// Map 类型测试 - Tree,完全相同的调用方式
List<Map<String, Object>> tree2 = Tree.buildTree(mapNodes,
map -> (Long) map.get("parentId"),
map -> (Long) map.get("id"),
(parent, child) -> {
@SuppressWarnings("unchecked")
List<Map<String, Object>> children =
(List<Map<String, Object>>) parent.get("children");
children.add(child);
});

这种”鸭子类型”般的兼容性体现了良好 API 设计的本质:关注行为而非形式,关注契约而非实现。它让我们能够在不破坏现有代码的前提下,无缝地从自定义接口迁移到标准接口,实现了真正的平滑升级。这也是很多项目在需要同时支持 JDK 7 和 JDK 8 时的常见操作——先定义兼容的自定义接口,这样既能在 JDK 8 环境中享受函数式编程的便利,又能保证在 JDK 7 环境中的正常运行,实现了跨版本的兼容性。


Java 树结构转换算法的再次优化:拥抱标准函数式接口
https://blog.mybatis.io/post/9b69116f.html
作者
Liuzh
发布于
2025年8月1日
许可协议