技术文章
>
Java
>
Java 树结构转换算法的再次优化:拥抱标准函数式接口
Java 树结构转换算法的再次优化:拥抱标准函数式接口
在前面的文章中,我们通过引入函数式接口成功解决了树结构转换算法的耦合性问题。但是,仔细观察会发现,我们定义的三个函数式接口实际上与 Java 8 标准函数式接口非常相似。本文将进一步优化实现,直接使用 Java 8 的标准函数式接口,让代码更加简洁和标准化。
从自定义接口到标准接口
图:左侧是优化后的实现,右侧为之前的实现
让我们来对比一下自定义接口与标准接口的映射关系:
1 2 3 4
| GetParentId<T, R> -> Function<R, T> GetId<T, R> -> Function<R, T> AddChild<R, C> -> BiConsumer<R, R>
|
这种映射是完全自然的:
GetParentId
和 GetId
都是接收一个参数并返回一个值,正好对应 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 {
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<>(); } 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()); } }
|
优化的关键变化:
- 移除自定义函数式接口:不再需要定义
GetParentId
、GetId
、AddChild
- 使用标准接口:直接使用
Function<R, T>
和 BiConsumer<R, R>
- 调用方法调整:从
.getId()
改为 .apply()
,从 .addChild()
改为 .accept()
鸭子类型:行为一致性的完美体现
在编程世界中,有一个著名的概念叫做”鸭子类型”(Duck Typing),它来自于这样一句话:
“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
虽然 Java 是静态类型语言,但在函数式编程的语境下,我们同样可以看到这种行为一致性的体现。TreeNewBee
和 Tree
就是一个完美的例子。
接口形式不同,行为完全一致
让我们来对比这两个类的方法签名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public static <T, R> List<R> buildTree( Collection<R> nodes, GetParentId<T, R> getParentId, // 自定义接口 GetId<T, R> getId, // 自定义接口 AddChild<R, R> addChild // 自定义接口 )
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
| List<Node> tree = TreeNewBee.buildTree( nodes, Node::getParentId, Node::getId, Node::addChild );
List<Node> tree = Tree.buildTree( nodes, Node::getParentId, Node::getId, Node::addChild );
|
为什么能够完全兼容?
这种兼容性的根本原因在于:
行为契约一致:两个接口虽然定义不同,但描述的是相同的行为契约
- 获取父节点ID:接收一个节点,返回其父节点ID
- 获取节点ID:接收一个节点,返回其ID
- 添加子节点:接收父节点和子节点,执行添加操作
方法引用的兼容性:Java 的方法引用具有强大的适配能力
1 2 3
| GetParentId<Integer, Node> getParentId = Node::getParentId; Function<Node, Integer> getParentId = Node::getParentId;
|
编译器的智能推断:编译器根据上下文自动推断出正确的函数式接口类型
实际验证
在我们的测试代码中,无论是简单的 Node 对象还是复杂的 Map 类型,都表现出完美的兼容性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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); });
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 环境中的正常运行,实现了跨版本的兼容性。