diff --git a/hutool-core/src/main/java/cn/hutool/core/tree/BeanTree.java b/hutool-core/src/main/java/cn/hutool/core/tree/BeanTree.java
new file mode 100644
index 000000000..bf5ac7aa1
--- /dev/null
+++ b/hutool-core/src/main/java/cn/hutool/core/tree/BeanTree.java
@@ -0,0 +1,207 @@
+package cn.hutool.core.tree;
+
+import cn.hutool.core.lang.Opt;
+import cn.hutool.core.lang.func.SerBiConsumer;
+import cn.hutool.core.lang.func.SerConsumer;
+import cn.hutool.core.lang.func.SerFunction;
+import cn.hutool.core.lang.func.SerPredicate;
+import cn.hutool.core.stream.EasyStream;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * 本类是用于构建树的工具类,特点是采取lambda,以及满足指定类型的Bean进行树操作
+ * Bean需要满足三个属性:
+ *
+ * - 包含不为null的主键(例如id)
+ * - 包含容许为null的关联外键(例如parentId)
+ * - 包含自身的子集,例如类型为List的children
+ *
+ * 本类的构建方法是通过{@code BeanTree.of} 进行构建,例如:
+ * {@code final BeanTree beanTree = BeanTree.of(JavaBean::getId, JavaBean::getParentId, null, JavaBean::getChildren, JavaBean::setChildren);}
+ * 得到的BeanTree实例可以调用toTree方法,将集合转换为树,例如:
+ * {@code final List javaBeanTree = beanTree.toTree(originJavaBeanList);}
+ * 也可以将已有的树转换为集合,例如:
+ * {@code final List javaBeanList = beanTree.flat(originJavaBeanTree);}
+ *
+ * @author VampireAchao
+ * 最后,引用一句电影经典台词: 无处安放的双手,以及无处安放的灵魂。——《Hello!树先生》
+ */
+public class BeanTree> {
+
+ /**
+ * 主键getter
+ */
+ private final SerFunction idGetter;
+ /**
+ * 外键getter
+ */
+ private final SerFunction pidGetter;
+ /**
+ * 外键匹配值(保留此属性主要是性能较外键条件匹配稍微好一点)
+ */
+ private final R pidValue;
+ /**
+ * 外键匹配条件
+ */
+ private final SerPredicate parentPredicate;
+ /**
+ * 子集getter
+ */
+ private final SerFunction> childrenGetter;
+ /**
+ * 子集setter
+ */
+ private final SerBiConsumer> childrenSetter;
+
+ private BeanTree(final SerFunction idGetter,
+ final SerFunction pidGetter,
+ final R pidValue,
+ final SerPredicate parentPredicate,
+ final SerFunction> childrenGetter,
+ final SerBiConsumer> childrenSetter) {
+ this.idGetter = idGetter;
+ this.pidGetter = pidGetter;
+ this.pidValue = pidValue;
+ this.parentPredicate = parentPredicate;
+ this.childrenGetter = childrenGetter;
+ this.childrenSetter = childrenSetter;
+ }
+
+ /**
+ * 构建BeanTree
+ *
+ * @param idGetter 主键getter,例如 {@code JavaBean::getId}
+ * @param pidGetter 外键getter,例如 {@code JavaBean::getParentId}
+ * @param pidValue 外键的值,例如 {@code null}
+ * @param childrenGetter 子集getter,例如 {@code JavaBean::getChildren}
+ * @param childrenSetter 子集setter,例如 {@code JavaBean::setChildren}
+ * @param Bean类型
+ * @param 主键、外键类型
+ * @return BeanTree
+ */
+ public static > BeanTree of(final SerFunction idGetter,
+ final SerFunction pidGetter,
+ final R pidValue,
+ final SerFunction> childrenGetter,
+ final SerBiConsumer> childrenSetter) {
+ return new BeanTree<>(idGetter, pidGetter, pidValue, null, childrenGetter, childrenSetter);
+ }
+
+ /**
+ * 构建BeanTree
+ *
+ * @param idGetter 主键getter,例如 {@code JavaBean::getId}
+ * @param pidGetter 外键getter,例如 {@code JavaBean::getParentId}
+ * @param parentPredicate 父节点判断条件,例如 {@code o -> Objects.isNull(o.getParentId())}
+ * @param childrenGetter 子集getter,例如 {@code JavaBean::getChildren}
+ * @param childrenSetter 子集setter,例如 {@code JavaBean::setChildren}
+ * @param Bean类型
+ * @param 主键、外键类型
+ * @return BeanTree
+ */
+ public static > BeanTree ofMatch(final SerFunction idGetter,
+ final SerFunction pidGetter,
+ final SerPredicate parentPredicate,
+ final SerFunction> childrenGetter,
+ final SerBiConsumer> childrenSetter) {
+ return new BeanTree<>(idGetter, pidGetter, null, parentPredicate, childrenGetter, childrenSetter);
+ }
+
+ /**
+ * 将集合转换为树
+ *
+ * @param list 集合
+ * @return 转换后的树
+ */
+ public List toTree(final List list) {
+ if (Objects.isNull(parentPredicate)) {
+ final Map> pIdValuesMap = EasyStream.of(list).filter(e -> Objects.nonNull(idGetter.apply(e))).group(pidGetter);
+ final List parents = pIdValuesMap.getOrDefault(pidValue, new ArrayList<>());
+ getChildrenFromMapByPidAndSet(pIdValuesMap);
+ return parents;
+ }
+ final List parents = new ArrayList<>(list.size());
+ final Map> pIdValuesMap = EasyStream.of(list).filter(e -> {
+ if (parentPredicate.test(e)) {
+ parents.add(e);
+ }
+ return Objects.nonNull(idGetter.apply(e));
+ }).group(pidGetter);
+ getChildrenFromMapByPidAndSet(pIdValuesMap);
+ return parents;
+ }
+
+ /**
+ * 将树扁平化为集合,相当于将树里的所有节点都放到一个集合里
+ *
+ * @param tree 树
+ * @return 集合
+ */
+ @SuppressWarnings("unchecked")
+ public List flat(final List tree) {
+ final AtomicReference>> recursiveRef = new AtomicReference<>();
+ final Function> recursive = e -> EasyStream.of(childrenGetter.apply(e)).flat(recursiveRef.get()).unshift(e);
+ recursiveRef.set(recursive);
+ return EasyStream.of(tree).flat(recursive).peek(e -> childrenSetter.accept(e, null)).toList();
+ }
+
+ /**
+ * 树的过滤操作,本方法一般适用于寻找某人所在部门以及所有上级部门类似的逻辑
+ * 通过{@link SerPredicate}指定的过滤规则,本节点或子节点满足过滤条件,则保留当前节点,否则抛弃节点及其子节点
+ *
+ * @param tree 树
+ * @param condition 节点过滤规则函数,只需处理本级节点本身即可,{@link SerPredicate#test(Object)}为{@code true}保留
+ * @return 过滤后的树
+ */
+ public List filter(final List tree, final SerPredicate condition) {
+ final AtomicReference> recursiveRef = new AtomicReference<>();
+ final Predicate recursive = SerPredicate.multiOr(condition::test,
+ e -> Opt.ofEmptyAble(childrenGetter.apply(e))
+ .map(children -> EasyStream.of(children).filter(recursiveRef.get()).toList())
+ .peek(children -> childrenSetter.accept(e, children))
+ .filter(s -> !s.isEmpty()).isPresent());
+ recursiveRef.set(recursive);
+ return EasyStream.of(tree).filter(recursive).toList();
+ }
+
+ /**
+ * 树节点遍历操作
+ *
+ * @param tree 数
+ * @param action 操作
+ * @return 树
+ */
+ public List forEach(final List tree, final SerConsumer action) {
+ final AtomicReference> recursiveRef = new AtomicReference<>();
+ final Consumer recursive = SerConsumer.multi(action::accept,
+ e -> Opt.ofEmptyAble(childrenGetter.apply(e))
+ .peek(children -> EasyStream.of(children).forEach(recursiveRef.get())));
+ recursiveRef.set(recursive);
+ EasyStream.of(tree).forEach(recursive);
+ return tree;
+ }
+
+ /**
+ * 内联函数,获取子集并设置到父节点
+ *
+ * @param pIdValuesMap 父id与子集的映射
+ */
+ private void getChildrenFromMapByPidAndSet(final Map> pIdValuesMap) {
+ EasyStream.of(pIdValuesMap.values()).flat(Function.identity())
+ .forEach(value -> {
+ final List children = pIdValuesMap.get(idGetter.apply(value));
+ if (children != null) {
+ childrenSetter.accept(value, children);
+ }
+ });
+ }
+
+}
diff --git a/hutool-core/src/test/java/cn/hutool/core/tree/BeanTreeTest.java b/hutool-core/src/test/java/cn/hutool/core/tree/BeanTreeTest.java
new file mode 100644
index 000000000..5db467bd9
--- /dev/null
+++ b/hutool-core/src/test/java/cn/hutool/core/tree/BeanTreeTest.java
@@ -0,0 +1,129 @@
+package cn.hutool.core.tree;
+
+import cn.hutool.core.stream.EasyStream;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.Tolerate;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Comparator;
+import java.util.List;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+/**
+ * TreeHelperTest
+ *
+ * @author VampireAchao
+ */
+public class BeanTreeTest {
+
+ @Data
+ @Builder
+ private static class JavaBean {
+ @Tolerate
+ public JavaBean() {
+ // this is an accessible parameterless constructor.
+ }
+
+ private String name;
+ private Integer age;
+ private Long id;
+ private Long parentId;
+ private List children;
+ private Boolean matchParent;
+ }
+
+ List originJavaBeanList;
+ List originJavaBeanTree;
+ BeanTree beanTree;
+
+ @Before
+ public void setUp() {
+ originJavaBeanList = EasyStream
+ .of(
+ JavaBean.builder().id(1L).name("dromara").matchParent(true).build(),
+ JavaBean.builder().id(2L).name("baomidou").matchParent(true).build(),
+ JavaBean.builder().id(3L).name("hutool").parentId(1L).build(),
+ JavaBean.builder().id(4L).name("sa-token").parentId(1L).build(),
+ JavaBean.builder().id(5L).name("mybatis-plus").parentId(2L).build(),
+ JavaBean.builder().id(6L).name("looly").parentId(3L).build(),
+ JavaBean.builder().id(7L).name("click33").parentId(4L).build(),
+ JavaBean.builder().id(8L).name("jobob").parentId(5L).build()
+ ).toList();
+ originJavaBeanTree = asList(
+ JavaBean.builder().id(1L).name("dromara").matchParent(true)
+ .children(asList(
+ JavaBean.builder().id(3L).name("hutool").parentId(1L)
+ .children(singletonList(JavaBean.builder().id(6L).name("looly").parentId(3L).build()))
+ .build(),
+ JavaBean.builder().id(4L).name("sa-token").parentId(1L)
+ .children(singletonList(JavaBean.builder().id(7L).name("click33").parentId(4L).build()))
+ .build()))
+ .build(),
+ JavaBean.builder().id(2L).name("baomidou").matchParent(true)
+ .children(singletonList(
+ JavaBean.builder().id(5L).name("mybatis-plus").parentId(2L)
+ .children(singletonList(
+ JavaBean.builder().id(8L).name("jobob").parentId(5L).build()
+ ))
+ .build()))
+ .build()
+ );
+ beanTree = BeanTree.of(JavaBean::getId, JavaBean::getParentId, null, JavaBean::getChildren, JavaBean::setChildren);
+ }
+
+ @Test
+ public void testToTree() {
+ final List javaBeanTree = beanTree.toTree(originJavaBeanList);
+ Assert.assertEquals(originJavaBeanTree, javaBeanTree);
+ final BeanTree conditionBeanTree = BeanTree.ofMatch(JavaBean::getId, JavaBean::getParentId, s -> Boolean.TRUE.equals(s.getMatchParent()), JavaBean::getChildren, JavaBean::setChildren);
+ Assert.assertEquals(originJavaBeanTree, conditionBeanTree.toTree(originJavaBeanList));
+ }
+
+ @Test
+ public void testFlat() {
+ final List javaBeanList = beanTree.flat(originJavaBeanTree);
+ javaBeanList.sort(Comparator.comparing(JavaBean::getId));
+ Assert.assertEquals(originJavaBeanList, javaBeanList);
+ }
+
+ @Test
+ public void testFilter() {
+ final List javaBeanTree = beanTree.filter(originJavaBeanTree, s -> "looly".equals(s.getName()));
+ Assert.assertEquals(singletonList(
+ JavaBean.builder().id(1L).name("dromara").matchParent(true)
+ .children(singletonList(JavaBean.builder().id(3L).name("hutool").parentId(1L)
+ .children(singletonList(JavaBean.builder().id(6L).name("looly").parentId(3L).build()))
+ .build()))
+ .build()),
+ javaBeanTree);
+ }
+
+ @Test
+ public void testForeach() {
+ final List javaBeanList = beanTree.forEach(originJavaBeanTree, s -> s.setName("【open source】" + s.getName()));
+ Assert.assertEquals(asList(
+ JavaBean.builder().id(1L).name("【open source】dromara").matchParent(true)
+ .children(asList(JavaBean.builder().id(3L).name("【open source】hutool").parentId(1L)
+ .children(singletonList(JavaBean.builder().id(6L).name("【open source】looly").parentId(3L).build()))
+ .build(),
+ JavaBean.builder().id(4L).name("【open source】sa-token").parentId(1L)
+ .children(singletonList(JavaBean.builder().id(7L).name("【open source】click33").parentId(4L).build()))
+ .build()))
+ .build(),
+ JavaBean.builder().id(2L).name("【open source】baomidou").matchParent(true)
+ .children(singletonList(
+ JavaBean.builder().id(5L).name("【open source】mybatis-plus").parentId(2L)
+ .children(singletonList(
+ JavaBean.builder().id(8L).name("【open source】jobob").parentId(5L).build()
+ ))
+ .build()))
+ .build()
+ ), javaBeanList);
+ }
+
+}