Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

icexmoon-tree

一个轻量级的 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);

foreach

节点类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,你可以从这里查看源码或提交建议。

版本更新记录

1.0.0

初始版本。

1.0.1

简化 POM 文件,移除不必要的依赖传递。修改 Readme 文件描述内容。

1.0.2

增加获取所有子节点的 API。

1.0.3

增加节点迭代器,可以更方便的对节点进行遍历操作。

The End.

About

一个轻量级的 Java 工具库,提供树形结构操作功能。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages