# 1. 引言
最近公司新进了不少新人,包括一些来自阿里、网易等大型企业的资深工程师。我们组的一位新同事是阿里来的专家,我在CR(Code Review, 简称CR)时看到了他编写的一个关于树操作的工具类,对其设计和实现深感佩服。为了进一步理解和学习,我对这个工具类进行了深入分析和整理,现在在本文中与大家分享。
# 2. 树形结构介绍
## 2.1 简单的二叉树
首页简单简介一下树形数据结构,树形数据结构是一种层级化的数据组织方式,广泛用于表示具有层次关系的数据。由于其层级化组织特点,树形数据结构能够高效支持多种查找、插入和删除操作,因此在计算机科学和实际应用中得到了广泛应用。下面是一个简单的二叉树示例:
![image.png](https://cdn.demongao.com/halo/image_1723684834347.png)
## 2.2 树的应用场景
树形数据结构的应用场景是通过ID关联上下级关系的对象,然后将这些对象组织成一棵树。主要有以下常用应用场景:
1. 部门通讯录:通讯录中可以通过树形结构展示不同部门及其上下级关系,便于用户快速找到联系人
2. 系统菜单:系统中的菜单通常是分层的,通过树形结构可以方便地展示和管理各级菜单项
3. 地址选择器:地理地址通常有多级关系,如省、市、县,通过树形结构可以方便用户选择具体的地址
4. 文件夹目录:文件系统中的文件夹和文件可以通过树形结构来组织和展示,便于用户进行文件操作
5. 产品多级分类: 通过树形结构可以直观地展示和管理各级分类
6. 评论回复: 通过树形结构可以展示帖子的回复关系,便于查看讨论的脉络
树形数据结构的应用场景通常是分层的,通过树形结构可以展示和管理各级流程节点及其关系。这些场景中,树形结构的应用可以显著提升数据的组织和展示效率,帮助用户更直观地理解和操作系统。
![image.png](https://cdn.demongao.com/halo/image_1723684910692.png)
# 3.JAVA中树形结构
## 3.1 JAVA中树形对象的定义
在JAVA中树形结构是通过对象嵌套方式来定义的,如MenuVo对象中有一个子对象subMenus:
```java
@Data
public class MenuVo {
private Long id;
private Long pId;
private String name;
private Integer rank=0;
private List<MenuVo> subMenus=new ArrayList<>();
}
```
## 3.2 JSON数据格式中的树形结构
JSON数据天然就是树形结果,如以下展示一个简单的JSON树形结构:
```
[
{
"id": 0,
"subMenus": [
{
"id": 2,
"subMenus": [
{
"id": 5,
"pid": 2
}
],
"pid": 0
}
],
"pid": -1
}
]
```
## 3.3 树形数据结构的储存
像文档型数据库MongDB、ElasticSearch可以直接存储JSON这种树形数据,而像Mysql这种关系型数据库不太适合直接存储具有上下级关系的树形数据,大都是按行存储然后通过id、pid之间关联上下级关系,这就导致我们需要经常将Mysql中的关系型数据转成JSON这种数据结构,因而TreeUtil这类数据转换工具类就起诞生了。
![image.png](https://cdn.demongao.com/halo/image_1723684983365.png)
# 4. TreeUtil代码分析
## 4.1 makeTree()构建树
直接看这神一样的方法makeTree():
```
public class TreeUtil {
public static <E> List<E> makeTree(List<E> list, Predicate<E> rootCheck, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> setSubChildren) {
return list.stream().filter(rootCheck).peek(x -> setSubChildren.accept(x, makeChildren(x, list, parentCheck, setSubChildren))).collect(Collectors.toList());
}
private static <E> List<E> makeChildren(E parent, List<E> allData, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> setSubChildren) {
return allData.stream().filter(x -> parentCheck.apply(parent, x)).peek(x -> setSubChildren.accept(x, makeChildren(x, allData, parentCheck, setSubChildren))).collect(Collectors.toList());
}
}
```
是不是完全看不懂?像看天书一样?makeTree方法为了通用使用了泛型+函数式编程+递归,正常人一眼根本看不这是在干什么的,我们先不用管这个makeTree合成树的代码原理,先直接看如何使用:
```
MenuVo menu0 = new MenuVo(0L, -1L);
MenuVo menu1 = new MenuVo(1L, 0L);
MenuVo menu2 = new MenuVo(2L, 0L);
MenuVo menu3 = new MenuVo(3L, 1L);
MenuVo menu4 = new MenuVo(4L, 1L);
MenuVo menu5 = new MenuVo(5L, 2L);
MenuVo menu6 = new MenuVo(6L, 2L);
MenuVo menu7 = new MenuVo(7L, 3L);
MenuVo menu8 = new MenuVo(8L, 3L);
MenuVo menu9 = new MenuVo(9L, 4L);
//基本数据
List<MenuVo> menuList = Arrays.asList(menu0,menu1, menu2,menu3,menu4,menu5,menu6,menu7,menu8,menu9);
//合成树
List<MenuVo> tree= TreeUtil.makeTree(menuList, x->x.getPId()==-1L,(x, y)->x.getId().equals(y.getPId()), MenuVo::setSubMenus);
System.out.println(JsonUtils.toJson(tree));
```
我们结合这个简单的合成菜单树看一下这个makeTree()方法参数是如何使用的:
* 第1个参数List list,为我们需要合成树的List,如上面Demo中的menuList
* 第2个参数Predicate rootCheck,判断为根节点的条件,如上面Demo中pId==-1就是根节点
* 第3个参数parentCheck 判断为父节点条件,如上面Demo中 id==pId
* 第4个参数setSubChildren,设置下级数据方法如上面Demo中:Menu::setSubMenus
有了上面这4个参数,只要是合成树场景,这个TreeUtil.makeTree()都可以适用,比如我们要合成一个部门树:
```
@Data
public class GroupVO {
private String groupId;
private String parentGroupId;
private String groupName;
private List<GroupVO> subGroups=new ArrayList<>();
}
```
groupId是部门ID, 根部门的条件是parentGroupId=null, 那么调用合成树的方法为:
```
List<GroupVO> groupTree=TreeUtil.makeTree(groupList, x->x.getParentGroupId==null,(x, y)->x.getGroupId().equals(y.getParentGroupId), GroupVO::setSubGroups);
```
是不是很优雅?很通用?完全不需要实现什么接口、定义什么TreeNode、增加什么TreeConfig,静态方法直接调用就搞定。一个字:绝!
# 5. 天书方法拆解
# 7. 总结
看完这位大神编写的TreeUtil工具类后,我深感佩服,其设计与实现真是令人叹为观止。该工具类不仅优雅且高效,使得以往需要递归处理的树形结构操作变得更加简洁和便捷。未来处理树形数据时,只需直接使用该工具类即可,无需再编写复杂的递归代码。
```
/**
* @Description: 树操作方法工具类
* @Author: 公众号:赵侠客
* @Copyright: Copyright (c) 赵侠客
* @Date: 2024-07-22 10:42
* @Version: 1.0
*/
public class TreeUtil {
/**
* 将list合成树
*
* @param list 需要合成树的List
* @param rootCheck 判断E中为根节点的条件,如:x->x.getPId()==-1L , x->x.getParentId()==null,x->x.getParentMenuId()==0
* @param parentCheck 判断E中为父节点条件,如:(x,y)->x.getId().equals(y.getPId())
* @param setSubChildren E中设置下级数据方法,如:Menu::setSubMenus
* @param <E> 泛型实体对象
* @return 合成好的树
*/
public static <E> List<E> makeTree(List<E> list, Predicate<E> rootCheck, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> setSubChildren) {
return list.stream().filter(rootCheck).peek(x -> setSubChildren.accept(x, makeChildren(x, list, parentCheck, setSubChildren))).collect(Collectors.toList());
}
/**
* 将树打平成tree
* @param tree 需要打平的树
* @param getSubChildren 设置下级数据方法,如:Menu::getSubMenus,x->x.setSubMenus(null)
* @param setSubChildren 将下级数据置空方法,如:x->x.setSubMenus(null)
* @return 打平后的数据
* @param <E> 泛型实体对象
*/
public static <E> List<E> flat(List<E> tree, Function<E, List<E>> getSubChildren, Consumer<E> setSubChildren) {
List<E> res = new ArrayList<>();
forPostOrder(tree, item -> {
setSubChildren.accept(item);
res.add(item);
}, getSubChildren);
return res;
}
/**
* 前序遍历
*
* @param tree 需要遍历的树
* @param consumer 遍历后对单个元素的处理方法,如:x-> System.out.println(x)、 System.out::println打印元素
* @param setSubChildren 设置下级数据方法,如:Menu::getSubMenus,x->x.setSubMenus(null)
* @param <E> 泛型实体对象
*/
public static <E> void forPreOrder(List<E> tree, Consumer<E> consumer, Function<E, List<E>> setSubChildren) {
for (E l : tree) {
consumer.accept(l);
List<E> es = setSubChildren.apply(l);
if (es != null && es.size() > 0) {
forPreOrder(es, consumer, setSubChildren);
}
}
}
/**
* 层序遍历
*
* @param tree 需要遍历的树
* @param consumer 遍历后对单个元素的处理方法,如:x-> System.out.println(x)、 System.out::println打印元素
* @param setSubChildren 设置下级数据方法,如:Menu::getSubMenus,x->x.setSubMenus(null)
* @param <E> 泛型实体对象
*/
public static <E> void forLevelOrder(List<E> tree, Consumer<E> consumer, Function<E, List<E>> setSubChildren) {
Queue<E> queue = new LinkedList<>(tree);
while (!queue.isEmpty()) {
E item = queue.poll();
consumer.accept(item);
List<E> childList = setSubChildren.apply(item);
if (childList != null && !childList.isEmpty()) {
queue.addAll(childList);
}
}
}
/**
* 后序遍历
*
* @param tree 需要遍历的树
* @param consumer 遍历后对单个元素的处理方法,如:x-> System.out.println(x)、 System.out::println打印元素
* @param setSubChildren 设置下级数据方法,如:Menu::getSubMenus,x->x.setSubMenus(null)
* @param <E> 泛型实体对象
*/
public static <E> void forPostOrder(List<E> tree, Consumer<E> consumer, Function<E, List<E>> setSubChildren) {
for (E item : tree) {
List<E> childList = setSubChildren.apply(item);
if (childList != null && !childList.isEmpty()) {
forPostOrder(childList, consumer, setSubChildren);
}
consumer.accept(item);
}
}
/**
* 对树所有子节点按comparator排序
*
* @param tree 需要排序的树
* @param comparator 排序规则Comparator,如:Comparator.comparing(MenuVo::getRank)按Rank正序 ,(x,y)->y.getRank().compareTo(x.getRank()),按Rank倒序
* @param getChildren 获取下级数据方法,如:MenuVo::getSubMenus
* @return 排序好的树
* @param <E> 泛型实体对象
*/
public static <E> List<E> sort(List<E> tree, Comparator<? super E> comparator, Function<E, List<E>> getChildren) {
for (E item : tree) {
List<E> childList = getChildren.apply(item);
if (childList != null && !childList.isEmpty()) {
sort(childList,comparator,getChildren);
}
}
tree.sort(comparator);
return tree;
}
private static <E> List<E> makeChildren(E parent, List<E> allData, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> children) {
return allData.stream().filter(x -> parentCheck.apply(parent, x)).peek(x -> children.accept(x, makeChildren(x, allData, parentCheck, children))).collect(Collectors.toList());
}
}
```
解密阿里大神写的天书般的Tree工具类,轻松搞定树结构!