一个轻量级的 Java 工具库,提供树形结构操作功能。
<dependency>
<groupId>cn.icexmoon</groupId>
<artifactId>icexmoon-tree</artifactId>
<version>1.0.0</version>
</dependency>假设需要用树构建的数据类型如下:
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
public class Department implements Nodeable<Department> {
@EqualsAndHashCode.Include
private Long id;
private String name;
private Long parentId;
private Department parent;
private List<Department> children;
public Department(Long id, String name, Long parentId) {
this.id = id;
this.name = name;
this.parentId = parentId;
}
}这里实现 Nodeable 接口并非必须,getter\setter\equals\hashcode 方法的实现也不是必须。
假设用 Department 构建了一些有树层级关系的测试数据:
public static final List<Department> departments = List.of(
new Department(1L, "宇宙科技有限公司", null),
new Department(2L, "人事部", 1L),
new Department(3L, "系统开发部", 1L),
new Department(4L, "开发一部", 3L),
new Department(5L, "开发二部", 3L),
new Department(6L, "OA项目组", 4L));这里 Department 实例的父子关系由 parentId 属性指定,当然这也不是必须的,树构建时关联关系依赖于用户提供的匿名函数,只要用户将怎么关联父子信息通过匿名函数告诉树即可。
通过构造函数创建树实例:
public static final Tree<Department> tree = new Tree<>(dept -> {
Long id = dept.getId();
List<Department> children = new ArrayList<>();
for (Department department : departments) {
if (department.getParentId() != null && department.getParentId().equals(id)) {
children.add(department);
}
}
return children;
}, departments::getFirst);Tree的构造器需要提供两个参数,都是匿名函数:
- 匿名函数 GetDirectChildren:提供一个要包含在树节点内的值(Value),返回其所有的子值(Child Value)。
- 匿名函数 GetRoot:返回根节点的值。
树的节点信息是保存在内存中的(Tree 对象的 root 字段),因此如果树创建时依赖的数据的父子关系发生变化,比如 department 表中新增了一个子部门或者删除了一个子部门。需要调用 API 销毁内存中的树结构,以便再次访问树时能重新构建树的父子关系。如果不这么做就可能导致一些 BUG。
销毁树:
tree.destroy();遍历树是一个常见操作,本项目支持通过一个匿名函数 Consumer 在遍历树时对节点执行一些操作:
tree.traversal(node -> {
List<Node<Department>> allParents = tree.getAllParents(node);
List<String> names = allParents.stream().map(n -> n.getValue().getName()).toList();
String fullNames = String.join("/", names);
System.out.println(fullNames);
});
getAllParents方法会返回指定节点的所有父节点,具体可以参考源码。- 默认采用深度优先遍历,提供其他 API 可以指定广度遍历或深度遍历。
除了使用匿名函数遍历,还提供多个迭代器进行遍历:
BreadthFirstChildrenIterator,对当前节点和子节点进行遍历,广度优先DepthFirstChildrenIterator,对当前节点和子节点进行遍历,深度优先ChildFirstParentsIterator,对当前节点和父节点进行遍历,从子节点到根节点ParentFirstParentsIterator,对当前节点和父节点进行遍历,从父节点到子节点
调用示例:
@Test
public void testDepthFirstIterator() {
Tree<Department> tree = TestData.tree;
DepthFirstChildrenIterator<Department> iterator = new DepthFirstChildrenIterator<>(tree.getRoot());
traversal(iterator);
}
private static void traversal(Iterator<Node<Department>> iterator) {
while (iterator.hasNext()){
Node<Department> node = iterator.next();
System.out.println(node.getValue());
}
}如果不需要遍历起始节点自身,只需要遍历其子节点(父节点),有构造器参数可以修改这一行为:
new DepthFirstChildrenIterator<>(tree.getRoot(), false);节点类Node实现了Iterable接口,默认返回一个深度优先遍历的迭代器,因此可以使用foreach语句遍历其子节点:
for (Node<Department> departmentNode : tree.getRoot()) {
System.out.println(departmentNode.getValue());
}如果需要使用深度优先的迭代器,可以:
tree.getRoot().setTraversalType(TraversalType.BREADTH_FIRST);
for (Node<Department> departmentNode : tree.getRoot()) {
System.out.println(departmentNode.getValue());
}如果需要遍历父节点或者遍历子节点时跳过起始节点,请使用迭代器。
有时候需要根据指定条件查找树上的某个节点,可以:
Node<Department> findNode = tree.findNode(node -> node.getChildren() != null && node.getValue().getId() == 5);
System.out.println(findNode.getValue());就像示例展示的,findNode方法接收一个 Predicate 类型的匿名函数作为匹配条件。
做前后端分离开发的时候,前端获取到的树不希望是类似:
{
value: {
id: 1,
name: "宇宙科技有限公司",
parentId: 0,
//...
},
children[
{
value: {
id: 2,
name: "人事部",
parentId: 1,
//...
},
children:[...]
}
]
}当然,直接使用是没有任何问题的,但如果前端一定要使用类似下面的“简单树”(不包含 Value 这一层)结构:
{
id: 1,
name: "宇宙科技有限公司",
parentId: 0,
//...
children[
{
id: 2,
name: "人事部",
parentId: 1,
//...
children:[...]
}
]
}提供一个 API 可以方便获取一个类似的简单树的拷贝:
Department rootDept = TreeUtil.getSimpleTreeCopy(TestData.tree, Department.class, false);
String jsonString = JSON.toJSONString(rootDept);
System.out.println(jsonString);
- 因为涉及对象拷贝,所以需要提供一个 Class 对象用于反射。
- 第三个参数可以控制返回的简单树中是否关联父节点(parent字段),一般来说需要 JSON 后返回给前端的话必须是
false,否则会导致 JSON 格式化工具循环遍历进而堆栈溢出。- 要操作的数据类型必须有 parent 和 children 属性以及对应的 Getter/Setter 方法,这里引入一个 Nodeable 接口进行限制,也就是说只有实现了该接口才能使用上面的 API。
其它相关的操作和 API 可以直接查看源码:
这里有一个使用本项目实现部门树的完整示例。
项目地址为 icexmoon/icexmoon-tree,你可以从这里查看源码或提交建议。
初始版本。
简化 POM 文件,移除不必要的依赖传递。修改 Readme 文件描述内容。
增加获取所有子节点的 API。
增加节点迭代器,可以更方便的对节点进行遍历操作。
The End.