From 924a124f7d5b12c64968b9ff7c2ac10c74c2429c Mon Sep 17 00:00:00 2001 From: wangyq Date: Mon, 6 Oct 2025 14:41:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=91=E5=BD=A2=E7=BB=93=E6=9E=84=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/cn/hutool/core/util/TreeUtil.java | 322 ++++++++++++++++++ .../cn/hutool/core/util/TreeUtilTest.java | 229 +++++++++++++ 2 files changed, 551 insertions(+) create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/TreeUtil.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/TreeUtilTest.java diff --git a/hutool-core/src/main/java/cn/hutool/core/util/TreeUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/TreeUtil.java new file mode 100644 index 0000000000..073524ff0f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/TreeUtil.java @@ -0,0 +1,322 @@ +package cn.hutool.core.util; + +import cn.hutool.core.collection.CollUtil; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 树形结构工具类 + * 用于处理具有父子关系的树形数据结构 + * + * @author wyq + */ +public class TreeUtil { + + /** + * 默认的父ID值 + */ + public static final String DEFAULT_PARENT_ID = "0"; + + /** + * 默认的根节点ID + */ + public static final String DEFAULT_ROOT_ID = "0"; + + /** + * 将列表结构转换为树形结构 + * + * @param 节点类型 + * @param list 节点列表 + * @param idGetter ID获取函数 + * @param parentIdGetter 父ID获取函数 + * @return 树形结构列表 + */ + public static List> listToTree(List list, + Function idGetter, + Function parentIdGetter) { + return listToTree(list, idGetter, parentIdGetter, DEFAULT_ROOT_ID); + } + + /** + * 将列表结构转换为树形结构 + * + * @param 节点类型 + * @param list 节点列表 + * @param idGetter ID获取函数 + * @param parentIdGetter 父ID获取函数 + * @param rootId 根节点ID + * @return 树形结构列表 + */ + public static List> listToTree(List list, + Function idGetter, + Function parentIdGetter, + String rootId) { + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + + // 构建ID到节点的映射 + Map> nodeMap = list.stream() + .map(item -> new TreeNode<>(item, idGetter.apply(item), parentIdGetter.apply(item))) + .collect(Collectors.toMap(TreeNode::getId, node -> node, (existing, replacement) -> replacement)); + + // 构建树形结构 + List> result = new ArrayList<>(); + for (TreeNode node : nodeMap.values()) { + if (rootId.equals(node.getParentId())) { + // 根节点 + result.add(node); + } else { + // 非根节点,找到父节点并添加为子节点 + TreeNode parentNode = nodeMap.get(node.getParentId()); + if (parentNode != null) { + parentNode.addChild(node); + } + } + } + + return result; + } + + /** + * 将树形结构扁平化为列表 + * + * @param 节点类型 + * @param treeList 树形结构列表 + * @return 扁平化后的列表 + */ + public static List treeToList(List> treeList) { + List result = new ArrayList<>(); + if (CollUtil.isEmpty(treeList)) { + return result; + } + + for (TreeNode node : treeList) { + result.add(node.getData()); + if (CollUtil.isNotEmpty(node.getChildren())) { + result.addAll(treeToList(node.getChildren())); + } + } + + return result; + } + + /** + * 获取指定节点的所有子节点(包括子孙节点) + * + * @param 节点类型 + * @param treeList 树形结构列表 + * @param targetId 目标节点ID + * @return 所有子节点列表 + */ + public static List> getChildren(List> treeList, String targetId) { + List> result = new ArrayList<>(); + if (CollUtil.isEmpty(treeList) || StrUtil.isBlank(targetId)) { + return result; + } + + for (TreeNode node : treeList) { + if (targetId.equals(node.getId())) { + // 找到目标节点,返回其所有子节点 + return getAllChildren(node); + } else if (CollUtil.isNotEmpty(node.getChildren())) { + // 递归查找子节点 + List> children = getChildren(node.getChildren(), targetId); + if (CollUtil.isNotEmpty(children)) { + return children; + } + } + } + + return result; + } + + /** + * 获取节点的所有子节点(递归) + * + * @param 节点类型 + * @param node 节点 + * @return 所有子节点列表 + */ + private static List> getAllChildren(TreeNode node) { + List> result = new ArrayList<>(); + if (node == null) { + return result; + } + + if (CollUtil.isNotEmpty(node.getChildren())) { + result.addAll(node.getChildren()); + for (TreeNode child : node.getChildren()) { + result.addAll(getAllChildren(child)); + } + } + + return result; + } + + /** + * 判断树中是否存在环形引用 + * + * @param 节点类型 + * @param treeList 树形结构列表 + * @return 是否存在环形引用 + */ + public static boolean hasCycle(List> treeList) { + if (CollUtil.isEmpty(treeList)) { + return false; + } + + Set visited = new HashSet<>(); + Set visiting = new HashSet<>(); + + for (TreeNode node : treeList) { + if (hasCycle(node, visited, visiting)) { + return true; + } + } + + return false; + } + + /** + * 递归检查节点是否存在环形引用 + * + * @param 节点类型 + * @param node 节点 + * @param visited 已访问节点集合 + * @param visiting 正在访问节点集合 + * @return 是否存在环形引用 + */ + private static boolean hasCycle(TreeNode node, Set visited, Set visiting) { + if (node == null) { + return false; + } + + String id = node.getId(); + if (visiting.contains(id)) { + // 发现环形引用 + return true; + } + + if (visited.contains(id)) { + // 已经检查过,无环形引用 + return false; + } + + // 标记为正在访问 + visiting.add(id); + + // 递归检查子节点 + if (CollUtil.isNotEmpty(node.getChildren())) { + for (TreeNode child : node.getChildren()) { + if (hasCycle(child, visited, visiting)) { + return true; + } + } + } + + // 标记为已访问 + visiting.remove(id); + visited.add(id); + + return false; + } + + /** + * 树节点类 + * + * @param 节点数据类型 + */ + public static class TreeNode { + /** + * 节点数据 + */ + private T data; + + /** + * 节点ID + */ + private String id; + + /** + * 父节点ID + */ + private String parentId; + + /** + * 子节点列表 + */ + private List> children; + + /** + * 构造函数 + * + * @param data 节点数据 + * @param id 节点ID + * @param parentId 父节点ID + */ + public TreeNode(T data, String id, String parentId) { + this.data = data; + this.id = id; + this.parentId = parentId; + this.children = new ArrayList<>(); + } + + /** + * 添加子节点 + * + * @param child 子节点 + */ + public void addChild(TreeNode child) { + if (this.children == null) { + this.children = new ArrayList<>(); + } + this.children.add(child); + } + + // Getter和Setter方法 + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public List> getChildren() { + return children; + } + + public void setChildren(List> children) { + this.children = children; + } + + @Override + public String toString() { + return "TreeNode{" + + "data=" + data + + ", id='" + id + '\'' + + ", parentId='" + parentId + '\'' + + ", children=" + children + + '}'; + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/TreeUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/TreeUtilTest.java new file mode 100644 index 0000000000..0f63002a58 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/TreeUtilTest.java @@ -0,0 +1,229 @@ +package cn.hutool.core.util; + +import lombok.Data; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 树形结构工具类测试 + * + * @author wyq + */ +public class TreeUtilTest { + + /** + * 测试数据类 + */ + @Data + static class Department { + private String id; + private String name; + private String parentId; + + public Department() { + } + + public Department(String id, String name, String parentId) { + this.id = id; + this.name = name; + this.parentId = parentId; + } + } + + @Test + public void testListToTree() { + // 准备测试数据 + List departments = new ArrayList<>(); + departments.add(new Department("1", "总公司", "0")); + departments.add(new Department("2", "北京分公司", "1")); + departments.add(new Department("3", "上海分公司", "1")); + departments.add(new Department("4", "研发部", "2")); + departments.add(new Department("5", "市场部", "2")); + departments.add(new Department("6", "人事部", "3")); + departments.add(new Department("7", "财务部", "3")); + + // 转换为树形结构 + List> tree = TreeUtil.listToTree( + departments, + Department::getId, + Department::getParentId + ); + + // 验证结果 + assertNotNull(tree); + assertEquals(1, tree.size()); + + TreeUtil.TreeNode root = tree.get(0); + assertEquals("1", root.getId()); + assertEquals("总公司", root.getData().getName()); + assertEquals(2, root.getChildren().size()); + + // 验证北京分公司 + TreeUtil.TreeNode beijing = root.getChildren().get(0); + assertEquals("2", beijing.getId()); + assertEquals("北京分公司", beijing.getData().getName()); + assertEquals(2, beijing.getChildren().size()); + + // 验证上海分公司 + TreeUtil.TreeNode shanghai = root.getChildren().get(1); + assertEquals("3", shanghai.getId()); + assertEquals("上海分公司", shanghai.getData().getName()); + assertEquals(2, shanghai.getChildren().size()); + } + + @Test + public void testTreeToList() { + // 准备树形结构数据 + List> tree = new ArrayList<>(); + + TreeUtil.TreeNode root = new TreeUtil.TreeNode<>(new Department("1", "总公司", "0"), "1", "0"); + TreeUtil.TreeNode beijing = new TreeUtil.TreeNode<>(new Department("2", "北京分公司", "1"), "2", "1"); + TreeUtil.TreeNode shanghai = new TreeUtil.TreeNode<>(new Department("3", "上海分公司", "1"), "3", "1"); + + root.addChild(beijing); + root.addChild(shanghai); + + tree.add(root); + + // 扁平化为列表 + List list = TreeUtil.treeToList(tree); + + // 验证结果 + assertNotNull(list); + assertEquals(3, list.size()); + + // 验证数据完整性 + boolean hasRoot = list.stream().anyMatch(d -> "1".equals(d.getId()) && "总公司".equals(d.getName())); + boolean hasBeijing = list.stream().anyMatch(d -> "2".equals(d.getId()) && "北京分公司".equals(d.getName())); + boolean hasShanghai = list.stream().anyMatch(d -> "3".equals(d.getId()) && "上海分公司".equals(d.getName())); + + assertTrue(hasRoot); + assertTrue(hasBeijing); + assertTrue(hasShanghai); + } + + @Test + public void testGetChildren() { + // 准备测试数据 + List departments = new ArrayList<>(); + departments.add(new Department("1", "总公司", "0")); + departments.add(new Department("2", "北京分公司", "1")); + departments.add(new Department("3", "上海分公司", "1")); + departments.add(new Department("4", "研发部", "2")); + departments.add(new Department("5", "市场部", "2")); + departments.add(new Department("6", "人事部", "3")); + departments.add(new Department("7", "财务部", "3")); + + // 转换为树形结构 + List> tree = TreeUtil.listToTree( + departments, + Department::getId, + Department::getParentId + ); + + // 获取总公司下的所有子节点 + List> children = TreeUtil.getChildren(tree, "1"); + + // 验证结果 + assertNotNull(children); + assertEquals(6, children.size()); // 包括所有子孙节点 + + // 验证包含的节点 + List childIds = children.stream().map(TreeUtil.TreeNode::getId).collect(Collectors.toList()); + assertTrue(childIds.contains("2")); // 北京分公司 + assertTrue(childIds.contains("3")); // 上海分公司 + assertTrue(childIds.contains("4")); // 研发部 + assertTrue(childIds.contains("5")); // 市场部 + assertTrue(childIds.contains("6")); // 人事部 + assertTrue(childIds.contains("7")); // 财务部 + } + + @Test + public void testHasCycle() { + // 准备正常数据(无环) + List normalDepartments = new ArrayList<>(); + normalDepartments.add(new Department("1", "总公司", "0")); + normalDepartments.add(new Department("2", "北京分公司", "1")); + normalDepartments.add(new Department("3", "上海分公司", "1")); + + List> normalTree = TreeUtil.listToTree( + normalDepartments, + Department::getId, + Department::getParentId + ); + + // 验证无环 + assertFalse(TreeUtil.hasCycle(normalTree)); + + // 准备有环数据 + TreeUtil.TreeNode node1 = new TreeUtil.TreeNode<>(new Department("1", "节点1", "0"), "1", "0"); + TreeUtil.TreeNode node2 = new TreeUtil.TreeNode<>(new Department("2", "节点2", "1"), "2", "1"); + TreeUtil.TreeNode node3 = new TreeUtil.TreeNode<>(new Department("3", "节点3", "2"), "3", "2"); + + // 构造环:node1 -> node2 -> node3 -> node1 + node1.addChild(node2); + node2.addChild(node3); + node3.addChild(node1); // 形成环 + + List> cycleTree = new ArrayList<>(); + cycleTree.add(node1); + + // 验证有环 + assertTrue(TreeUtil.hasCycle(cycleTree)); + } + + @Test + public void testEmptyList() { + // 测试空列表转换 + List> tree = TreeUtil.listToTree( + new ArrayList<>(), + Department::getId, + Department::getParentId + ); + + assertNotNull(tree); + assertTrue(tree.isEmpty()); + } + + @Test + public void testNullParameters() { + // 测试null参数 - 应该返回空列表而不是抛出异常 + List> result = TreeUtil.listToTree( + null, + Department::getId, + Department::getParentId + ); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void testNullFunctionParameters() { + // 测试函数参数为null的情况 + List departments = new ArrayList<>(); + departments.add(new Department("1", "总公司", "0")); + + // 测试idGetter为null + assertThrows(NullPointerException.class, () -> { + TreeUtil.listToTree( + departments, + null, + Department::getParentId + ); + }); + + // 测试parentIdGetter为null + assertThrows(NullPointerException.class, () -> { + TreeUtil.listToTree( + departments, + Department::getId, + null + ); + }); + } +} -- Gitee