diff --git a/hutool-core/src/main/java/cn/hutool/core/stream/EasyStream.java b/hutool-core/src/main/java/cn/hutool/core/stream/EasyStream.java index 9460a88e9..dbc35d1e0 100644 --- a/hutool-core/src/main/java/cn/hutool/core/stream/EasyStream.java +++ b/hutool-core/src/main/java/cn/hutool/core/stream/EasyStream.java @@ -1364,6 +1364,95 @@ public class EasyStream implements Stream, Iterable { return collect(CollectorUtil.toMap(keyMapper, valueMapper, mergeFunction, mapSupplier)); } + /** + * 将集合转换为树,默认用 {@code parentId == null} 作为顶部,内置一个小递归 + * 因为需要在当前传入数据里查找,所以这是一个结束操作 + * + * @param idGetter id的getter对应的lambda,可以写作 {@code Student::getId} + * @param pIdGetter parentId的getter对应的lambda,可以写作 {@code Student::getParentId} + * @param childrenSetter children的setter对应的lambda,可以写作{ @code Student::setChildren} + * @param 此处是id、parentId的泛型限制 + * @return list 组装好的树 + * eg: + * {@code List studentTree = EasyStream.of(students).toTree(Student::getId, Student::getParentId, Student::setChildren) } + */ + public > List toTree(Function idGetter, + Function pIdGetter, + BiConsumer> childrenSetter) { + Map> pIdValuesMap = group(pIdGetter); + return getChildrenFromMapByPidAndSet(idGetter, childrenSetter, pIdValuesMap, pIdValuesMap.get(null)); + } + + /** + * 将集合转换为树,自定义树顶部的判断条件,内置一个小递归(没错,lambda可以写递归) + * 因为需要在当前传入数据里查找,所以这是一个结束操作 + * + * @param idGetter id的getter对应的lambda,可以写作 {@code Student::getId} + * @param pIdGetter parentId的getter对应的lambda,可以写作 {@code Student::getParentId} + * @param childrenSetter children的setter对应的lambda,可以写作 {@code Student::setChildren} + * @param parentPredicate 树顶部的判断条件,可以写作 {@code s -> Objects.equals(s.getParentId(),0L) } + * @param 此处是id、parentId的泛型限制 + * @return list 组装好的树 + * eg: + * {@code List studentTree = EasyStream.of(students).toTree(Student::getId, Student::getParentId, Student::setChildren, Student::getMatchParent) } + */ + + public > List toTree(Function idGetter, + Function pIdGetter, + BiConsumer> childrenSetter, + Predicate parentPredicate) { + List list = toList(); + List parents = EasyStream.of(list).filter(e -> + // 此处是为了适配 parentPredicate.test空指针 情况 + // 因为Predicate.test的返回值是boolean,所以如果 e -> null 这种返回null的情况,会直接抛出NPE + Opt.ofTry(() -> parentPredicate.test(e)).filter(Boolean::booleanValue).isPresent()) + .toList(); + return getChildrenFromMapByPidAndSet(idGetter, childrenSetter, EasyStream.of(list).group(pIdGetter), parents); + } + + /** + * toTree的内联函数,内置一个小递归(没错,lambda可以写递归) + * 因为需要在当前传入数据里查找,所以这是一个结束操作 + * + * @param idGetter id的getter对应的lambda,可以写作 {@code Student::getId} + * @param childrenSetter children的setter对应的lambda,可以写作 {@code Student::setChildren} + * @param pIdValuesMap parentId和值组成的map,用来降低复杂度 + * @param parents 顶部数据 + * @param 此处是id的泛型限制 + * @return list 组装好的树 + */ + private > List getChildrenFromMapByPidAndSet(Function idGetter, + BiConsumer> childrenSetter, + Map> pIdValuesMap, + List parents) { + MutableObj>> recursiveRef = new MutableObj<>(); + Consumer> recursive = values -> EasyStream.of(values, isParallel()).forEach(value -> { + List children = pIdValuesMap.get(idGetter.apply(value)); + childrenSetter.accept(value, children); + recursiveRef.get().accept(children); + }); + recursiveRef.set(recursive); + recursive.accept(parents); + return parents; + } + + /** + * 将树递归扁平化为集合,内置一个小递归(没错,lambda可以写递归) + * 这是一个无状态中间操作 + * + * @param childrenGetter 获取子节点的lambda,可以写作 {@code Student::getChildren} + * @param childrenSetter 设置子节点的lambda,可以写作 {@code Student::setChildren} + * @return EasyStream 一个流 + * eg: + * {@code List students = EasyStream.of(studentTree).flatTree(Student::getChildren, Student::setChildren).toList() } + */ + public EasyStream flatTree(Function> childrenGetter, BiConsumer> childrenSetter) { + MutableObj>> recursiveRef = new MutableObj<>(); + Function> recursive = e -> EasyStream.of(childrenGetter.apply(e)).flat(recursiveRef.get()).unshift(e); + recursiveRef.set(recursive); + return flat(recursive).peek(e -> childrenSetter.accept(e, null)); + } + /** * 通过给定分组依据进行分组 diff --git a/hutool-core/src/test/java/cn/hutool/core/stream/EasyStreamTest.java b/hutool-core/src/test/java/cn/hutool/core/stream/EasyStreamTest.java index e07ad4a4e..098fc12b7 100644 --- a/hutool-core/src/test/java/cn/hutool/core/stream/EasyStreamTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/stream/EasyStreamTest.java @@ -3,13 +3,18 @@ package cn.hutool.core.stream; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.Tolerate; import org.junit.Assert; import org.junit.Test; import java.util.*; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.util.Arrays.asList; import static java.util.Collections.singletonList; /** @@ -427,6 +432,126 @@ public class EasyStreamTest { Assert.assertTrue(EasyStream.of(1).isNotEmpty()); } + @Test + public void testToTree() { + Consumer test = o -> { + List studentTree = EasyStream + .of( + Student.builder().id(1L).name("dromara").build(), + Student.builder().id(2L).name("baomidou").build(), + Student.builder().id(3L).name("hutool").parentId(1L).build(), + Student.builder().id(4L).name("sa-token").parentId(1L).build(), + Student.builder().id(5L).name("mybatis-plus").parentId(2L).build(), + Student.builder().id(6L).name("looly").parentId(3L).build(), + Student.builder().id(7L).name("click33").parentId(4L).build(), + Student.builder().id(8L).name("jobob").parentId(5L).build() + ) + // just 3 lambda,top parentId is null + .toTree(Student::getId, Student::getParentId, Student::setChildren); + Assert.assertEquals(asList( + Student.builder().id(1L).name("dromara") + .children(asList(Student.builder().id(3L).name("hutool").parentId(1L) + .children(singletonList(Student.builder().id(6L).name("looly").parentId(3L).build())) + .build(), + Student.builder().id(4L).name("sa-token").parentId(1L) + .children(singletonList(Student.builder().id(7L).name("click33").parentId(4L).build())) + .build())) + .build(), + Student.builder().id(2L).name("baomidou") + .children(singletonList( + Student.builder().id(5L).name("mybatis-plus").parentId(2L) + .children(singletonList( + Student.builder().id(8L).name("jobob").parentId(5L).build() + )) + .build())) + .build() + ), studentTree); + }; + test = test.andThen(o -> { + List studentTree = EasyStream + .of( + Student.builder().id(1L).name("dromara").matchParent(true).build(), + Student.builder().id(2L).name("baomidou").matchParent(true).build(), + Student.builder().id(3L).name("hutool").parentId(1L).build(), + Student.builder().id(4L).name("sa-token").parentId(1L).build(), + Student.builder().id(5L).name("mybatis-plus").parentId(2L).build(), + Student.builder().id(6L).name("looly").parentId(3L).build(), + Student.builder().id(7L).name("click33").parentId(4L).build(), + Student.builder().id(8L).name("jobob").parentId(5L).build() + ) + // just 4 lambda ,top by condition + .toTree(Student::getId, Student::getParentId, Student::setChildren, Student::getMatchParent); + Assert.assertEquals(asList( + Student.builder().id(1L).name("dromara").matchParent(true) + .children(asList(Student.builder().id(3L).name("hutool").parentId(1L) + .children(singletonList(Student.builder().id(6L).name("looly").parentId(3L).build())) + .build(), + Student.builder().id(4L).name("sa-token").parentId(1L) + .children(singletonList(Student.builder().id(7L).name("click33").parentId(4L).build())) + .build())) + .build(), + Student.builder().id(2L).name("baomidou").matchParent(true) + .children(singletonList( + Student.builder().id(5L).name("mybatis-plus").parentId(2L) + .children(singletonList( + Student.builder().id(8L).name("jobob").parentId(5L).build() + )) + .build())) + .build() + ), studentTree); + }); + test.accept(new Object()); + } + + @Test + public void testFlatTree() { + List studentTree = asList( + Student.builder().id(1L).name("dromara") + .children(asList(Student.builder().id(3L).name("hutool").parentId(1L) + .children(singletonList(Student.builder().id(6L).name("looly").parentId(3L).build())) + .build(), + Student.builder().id(4L).name("sa-token").parentId(1L) + .children(singletonList(Student.builder().id(7L).name("click33").parentId(4L).build())) + .build())) + .build(), + Student.builder().id(2L).name("baomidou") + .children(singletonList( + Student.builder().id(5L).name("mybatis-plus").parentId(2L) + .children(singletonList( + Student.builder().id(8L).name("jobob").parentId(5L).build() + )) + .build())) + .build() + ); + Assert.assertEquals(asList( + Student.builder().id(1L).name("dromara").build(), + Student.builder().id(2L).name("baomidou").build(), + Student.builder().id(3L).name("hutool").parentId(1L).build(), + Student.builder().id(4L).name("sa-token").parentId(1L).build(), + Student.builder().id(5L).name("mybatis-plus").parentId(2L).build(), + Student.builder().id(6L).name("looly").parentId(3L).build(), + Student.builder().id(7L).name("click33").parentId(4L).build(), + Student.builder().id(8L).name("jobob").parentId(5L).build() + ), EasyStream.of(studentTree).flatTree(Student::getChildren, Student::setChildren).sorted(Comparator.comparingLong(Student::getId)).toList()); + + } + + @Data + @Builder + public static class Student { + private String name; + private Integer age; + private Long id; + private Long parentId; + private List children; + private Boolean matchParent = false; + + @Tolerate + public Student() { + // this is an accessible parameterless constructor. + } + } + @Test public void testTransform() { final boolean result = EasyStream.of(1, 2, 3)