我是如何给阿里大神Tree工具类做CodeReview并优化的

news2025/1/11 7:07:22

首发公众号:赵侠客

引言

前段时间我写了一篇关于树操作的工具类《解密阿里大神写的天书般的Tree工具类,轻松搞定树结构!》,当时主要把精力集中在分析代码的实现层面,没有从设计层面、性能层考虑,然后就被很多网友大神喷了,在此我做下深刻的反思,同时也针对大神网友提的缺点做出优化。
主要的缺点:

  1. 性能问题,因为使用递归,最坏的情况时间复杂度为 O(n^n),平均为O(nlogn),性能确实非常差
  2. 使用了peek(),peak()坑确实比较多,官方建议仅在调试和日志记录时使用,避免在 peek() 中修改元素状态
  3. 没有filter、search等方法,过滤和搜索也是树形结构中常用的方法

本文接针对这三点做了优化

一、性能优化

1.1 时间复杂度降到O(n)

使用网友的建议合成树使用Map用空间来换时间,将时间复杂度降到O(n),我们直接看优化后的代码:

public static <T, E> List<E> makeTree(List<E> menuList, Function<E, T> pId, Function<E, T> id, Predicate<E> rootCheck, BiConsumer<E, List<E>> setSubChildren) {
    //按原数组顺序构建父级数据Map,使用Optional考虑pId为null
    Map<Optional<T>, List<E>> parentMenuMap = menuList.stream().collect(Collectors.groupingBy(
            node -> Optional.ofNullable(pId.apply(node)),
            LinkedHashMap::new,
            Collectors.toList()
    ));
    List<E> result = new ArrayList<>();
    for (E node : menuList) {
        //添加到下级数据中
        setSubChildren.accept(node, parentMenuMap.get(Optional.ofNullable(id.apply(node))));
        //如里是根节点,加入结构
        if (rootCheck.test(node)) {
            result.add(node);
        }
    }
    return result;
}

方法参数说明:

  1. List menuList,需要合成的集全数据
  2. Function<E, T> pId,实体中的父级ID字段,可以为null,如:MenuVo::getPId
  3. Function<E, T> id,实体中的ID字段,不能为null,如,MenuVo::getId
  4. Predicate rootCheck,判断为根节点条件,如: x->x.getPId()==-1L
  5. BiConsumer<E, List> setSubChildren,设置子节点方法,如:MenuVo::setSubMenus

使用方法:

//定义GroupVo
public class GroupVo {
    private String groupId;
    private String parentGroupId;
    private List<GroupVo> subGroups;
}
//测试合成树
GroupVo groupVo1=new GroupVo("a",null);
GroupVo groupVo2=new GroupVo("b",null);
GroupVo groupVo3=new GroupVo("c","a");
GroupVo groupVo4=new GroupVo("d","b");
List<GroupVo> groupVos= Arrays.asList(groupVo1,groupVo2,groupVo3,groupVo4);
List<GroupVo> tree=TreeUtil.makeTree(groupVos,GroupVo::getParentGroupId,GroupVo::getGroupId,x->x.getParentGroupId()==null,GroupVo::setSubGroups);
System.out.println(JsonUtils.toJson(tree));

输出结果:

[
    {
        "id": "a",
        "subGroup": [
            {
                "id": "c",
                "pid": "a"
            }
        ]
    },
    {
        "id": "b",
        "subGroup": [
            {
                "id": "d",
                "pid": "b"
            }
        ]
    }
]

1.2 性能对比测试

下图为优化后的makeTree和之前的方法分别对1万、2万、3万、4万、5万、6万节点进行耗时测试对比:

递归Map耗时对比

图:Map和递归耗时对比

通过对比可以看出递归实现耗时会随着节点增长成指数增长,使用Map的耗时就小的多了,性能确实提升了好几个数量级。

1.3 Map实现原理:

使用Map空间换时间的思路其实并不复杂,主要有三步:

  1. 通过pId构建父级到下子级到倒排序索引,如图中的1下级有3和4
  2. 遍历原List,将所有节点首尾相连
  3. 返回我们需要的根节点

使用Map合成树实现过程

图:使用Map合成树实现过程

图中 1-0 表示id=1,pId=0 ,右边为分组后的Map,我们看循环过程

  1. 1-0,在Map中通过1找到下级节点为3,4,然后设置到1的下级
  2. 2-0,在Map中通过2打到下级节点为5,然后设置到2的下级
  3. 3-1,在Map中没有打到下级
  4. 4-1,在Map中通过4打到下级节点为6,然后设置到4的下级
  5. 5-2,在Map中没有打到下级
  6. 6-4,在Map中没有打到下级

使用Map合成树代码虽然比较简单,但是还是有几点需要注意的:

  1. 使用Map空间换时间,将时间复杂度降为O(n),占用内存会增大
  2. 使用Optional类型支持pId为null
  3. 使用LinkedHashMap,保证合成树后子节点相对顺序不变
  4. 使用for替代peak(),避免peak的坑

1.4 大数据量测试

有网说他们有500万节点合成树的需求,在我做的所有项目中没有遇到过一次性合成500万节点树的需求,我想应该不是WEB或是移动应用,这500万节点树返回给前端,我想前端会直接卡死。不管有没有应用场景我们可以测试一下500万节点,树的深度为100的超级大树合成速度,结果只用了424ms,性能是不是炸裂了?

500万深度100

图:500万节点耗时424ms

再看一下makeTree方法内部耗时:
各部分耗时

图:makeTree各方法耗时

可以看出主要耗时在是通过pId分组这里,耗时316ms。

测试代码:

 //树节点数
 public static Integer size = 5000000;
 //树深度
 public static Integer deep = 100;
 private static List<MenuVo> menuVos = new ArrayList<>();
 @BeforeAll
 public static void init() {
     long currentId = 1;
     List<MenuVo> currentLevel = new ArrayList<>();
     MenuVo root = new MenuVo(currentId++, 0L, "Root");
     menuVos.add(root);
     currentLevel.add(root);
     for (int level = 1; level < deep && currentId <= size; level++) {
         List<MenuVo> nextLevel = new ArrayList<>();
         for (MenuVo parent : currentLevel) {
             for (int i = 0; i < size / Math.pow(10, level); i++) {
                 if (currentId > size) break;
                 MenuVo child = new MenuVo(currentId++, parent.getId(), String.format("关注公众号:【赵侠客】%s", currentId));
                 menuVos.add(child);
                 nextLevel.add(child);
             }
         }
         currentLevel = nextLevel;
     }
 }

 @Test
 public void testBigTree() {
     List<MenuVo> tree = TreeUtil.makeTree(menuVos, MenuVo::getPId, MenuVo::getId, x -> x.getPId() == 0, MenuVo::setSubMenus);
 }

二、 peak()坑总结

Java Stream 的 peek() 方法是一种中间操作,通常用于在流的每个元素上执行某些操作(例如,日志记录或调试)。然而,由于其特殊的性质和某些误解,它在实际业务处理中可能会带来一些潜在的坑。以下是一些使用 peek() 时需要注意的坑:

2.1 peek() 是中间操作,不会触发流的执行

peek() 方法是一个中间操作,这意味着它不会单独触发流的执行。只有在终端操作(如 collect()、forEach()、reduce() 等)被调用时,流才会被执行。如果仅仅使用 peek() 而没有终端操作,流中的操作将不会被执行。

Stream.of(1, 2, 3)
      .peek(System.out::println); // 不会打印任何内容,因为没有终端操作

2.2 peek() 不应该用于改变元素的状态

peek() 的设计初衷是用于查看每个元素,而不是修改元素。如果在 peek() 中修改元素的状态,可能会导致意料之外的副作用,尤其是在并行流中。

List<String> list = Arrays.asList("a", "b", "c");
list.stream()
    .peek(s -> s = s.toUpperCase()) // 试图修改元素,实际上不会生效
    .forEach(System.out::println);  // 打印 "a", "b", "c" 而不是 "A", "B", "C"

2.3 在并行流中使用 peek() 可能导致线程安全问题

在并行流中,流的每个元素可能会被不同的线程处理。如果 peek() 中的操作不是线程安全的,可能会导致不可预知的结果或数据竞争。

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = Collections.synchronizedList(new ArrayList<>());
list.parallelStream()
    .peek(result::add)
    .forEach(System.out::println); //可能导致 ConcurrentModificationException 或不完整的结果

2.4 peek() 的操作顺序和流的执行顺序有关

peek() 中的操作顺序与流的执行顺序有关。如果流中有多个中间操作,peek() 的操作可能不会按照预期的顺序执行。

Stream.of("one", "two", "three")
      .peek(s -> System.out.println("Peek1: " + s))
      .filter(s -> s.length() > 3)
      .peek(s -> System.out.println("Peek2: " + s))
      .forEach(System.out::println);
// 可能输出:
// Peek1: one
// Peek1: two
// Peek1: three
// Peek2: three
// three

2.5 peak小结

虽然 peek() 方法在调试和日志记录时非常有用,但在实际业务处理中应谨慎使用。为避免潜在的问题,通常建议使用其他方法来完成需要修改元素状态或线程安全的操作,有三点建议:

  1. 仅在调试和日志记录时使用 peek()。
  2. 避免在 peek() 中修改元素状态。
  3. 确保 peek() 中的操作是线程安全的,特别是在并行流中使用时。

三、扩展方法 filter,search方法

3.1 filter()方法

filter()方法和Stream的filter()方法一样,过滤满足条件的数据节点,如里当前节点不满足其所有子节点都会过滤掉

public static <E> List<E> filter(List<E> tree, Predicate<E> predicate, Function<E, List<E>> getChildren) {
    return tree.stream().filter(item -> {
        if (predicate.test(item)) {
            List<E> children = getChildren.apply(item);
            if (children != null && !children.isEmpty()) {
                filter(children, predicate, getChildren);
            }
            return true;
        }
        return false;
    }).collect(Collectors.toList());
}

filter()使用方法:

//只保留id<5的节点
List<MenuVo> filterMenus =TreeUtil.filter(tree1,x->x.getId()<5,MenuVo::getSubMenus);

3.2 search()方法

search()方法搜索子节点,并且返回到根节点路径上的所有节点,通常用在前端多级分类搜索中,如下图:

树的搜索

图:element UI Tree组件的搜索

z ```java public static List search(List tree, Predicate predicate, Function

search()使用方法
```java
MenuVo menu0 = new MenuVo(0L, -1L);
menu0.setName("自媒体");
MenuVo menu1 = new MenuVo(1L, 0L);
menu1.setName("公众号");
MenuVo menu2 = new MenuVo(2L, 0L);
menu2.setName("掘金");
MenuVo menu3 = new MenuVo(3L, 1L);
menu3.setName("赵侠客");
MenuVo menu4 = new MenuVo(4L, 1L);
MenuVo menu5 = new MenuVo(5L, 2L);
MenuVo menu6 = new MenuVo(6L, 2L);
menu6.setName("赵侠客");
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> tree1= TreeUtil.makeTree(menuList, MenuVo::getPId,MenuVo::getId,x->x.getPId()==-1L, MenuVo::setSubMenus);
System.out.println(JsonUtils.toJson(tree1));
List<MenuVo> searchRes = TreeUtil.search(tree1,x->"赵侠客".equals(x.getName()) ,MenuVo::getSubMenus);
System.out.println(JsonUtils.toJson(searchRes));

输入树:

[
    {
        "id": 0,
        "name": "自媒体",
        "subMenus": [
            {
                "id": 1,
                "name": "公众号",
                "subMenus": [
                    {
                        "id": 3,
                        "name": "赵侠客",
                        "subMenus": [
                            {
                                "id": 7,
                                "pid": 3
                            },
                            {
                                "id": 8,
                                "pid": 3
                            }
                        ],
                        "pid": 1
                    },
                    {
                        "id": 4,
                        "subMenus": [
                            {
                                "id": 9,
                                "pid": 4
                            }
                        ],
                        "pid": 1
                    }
                ],
                "pid": 0
            },
            {
                "id": 2,
                "name": "掘金",
                "subMenus": [
                    {
                        "id": 5,
                        "pid": 2
                    },
                    {
                        "id": 6,
                        "name": "赵侠客",
                        "pid": 2
                    }
                ],
                "pid": 0
            }
        ],
        "pid": -1
    }
]

搜索赵侠客后返回的结果:

[
    {
        "id": 0,
        "name": "自媒体",
        "subMenus": [
            {
                "id": 1,
                "name": "公众号",
                "subMenus": [
                    {
                        "id": 3,
                        "name": "赵侠客",
                        "subMenus": [],
                        "pid": 1
                    }
                ],
                "pid": 0
            },
            {
                "id": 2,
                "name": "掘金",
                "subMenus": [
                    {
                        "id": 6,
                        "name": "赵侠客",
                        "pid": 2
                    }
                ],
                "pid": 0
            }
        ],
        "pid": -1
    }
]

总结

借助这次广大网友对我代码的CodeReview我总结反思了以下几点:

  1. 不要做井底之蛙,每个人的代码水平、认知水平、知识领域都不一样,有时自己觉得很好的东西,在高一层次的人看来可能就是垃圾,有时自己觉得很好的方案,在高一层的人看来就是幼稚,所以在团队开发过程技术方案评审及CodeReview还是很重要的
  2. 吾日三省吾身,要勇于接受别的观点、建议、意见和指责,千万不要和别人打嘴炮,自己的提升成长才是最重要的
  3. 做事、思考、总结,首先应该先好好做事,做完后要思考有没有更好的做法,最后要总结记录下来

完整老代码参考:解密阿里大神写的天书般的Tree工具类,轻松搞定树结构!

完整优化后的代码:

/**
 * @Description: 树操作方法工具类
 * @Author: 公众号:赵侠客
 * @Copyright: Copyright (c) 赵侠客
 * @Date: 2024-07-22 10:42
 * @Version: 1.0
 */
public class TreeUtil {
    /**
     * 使用Map合成树
     *
     * @param menuList       需要合成树的List
     * @param pId            对象中的父ID字段,如:Menu:getPid
     * @param id             对象中的id字段 ,如:Menu:getId
     * @param rootCheck      判断E中为根节点的条件,如:x->x.getPId()==-1L , x->x.getParentId()==null,x->x.getParentMenuId()==0
     * @param setSubChildren E中设置下级数据方法,如: Menu::setSubMenus
     * @param <T>            ID字段类型
     * @param <E>            泛型实体对象
     * @return
     */
    public static <T, E> List<E> makeTree(List<E> menuList, Function<E, T> pId, Function<E, T> id, Predicate<E> rootCheck, BiConsumer<E, List<E>> setSubChildren) {
        //按原数组顺序构建父级数据Map,使用Optional考虑pId为null
        Map<Optional<T>, List<E>> parentMenuMap = menuList.stream().collect(Collectors.groupingBy(
                node -> Optional.ofNullable(pId.apply(node)),
                LinkedHashMap::new,
                Collectors.toList()
        ));
        List<E> result = new ArrayList<>();
        for (E node : menuList) {
            //添加到下级数据中
            setSubChildren.accept(node, parentMenuMap.get(Optional.ofNullable(id.apply(node))));
            //如里是根节点,加入结构
            if (rootCheck.test(node)) {
                result.add(node);
            }
        }
        return result;
    }

    /**
     * 树中过滤
     * @param tree  需要过滤的树
     * @param predicate  过滤条件
     * @param getChildren 获取下级数据方法,如:MenuVo::getSubMenus
     * @return List<E> 过滤后的树
     * @param <E> 泛型实体对象
     */
    public static <E> List<E> filter(List<E> tree, Predicate<E> predicate, Function<E, List<E>> getChildren) {
        return tree.stream().filter(item -> {
            if (predicate.test(item)) {
                List<E> children = getChildren.apply(item);
                if (children != null && !children.isEmpty()) {
                    filter(children, predicate, getChildren);
                }
                return true;
            }
            return false;
        }).collect(Collectors.toList());
    }


    /**
     * 树中搜索
     * @param tree
     * @param predicate
     * @param getSubChildren
     * @return 返回搜索到的节点及其父级到根节点
     * @param <E>
     */
    public static <E> List<E> search(List<E> tree, Predicate<E> predicate, Function<E, List<E>> getSubChildren) {
        Iterator<E> iterator = tree.iterator();
        while (iterator.hasNext()) {
            E item = iterator.next();
            List<E> childList = getSubChildren.apply(item);
            if (childList != null && !childList.isEmpty()) {
                search(childList, predicate, getSubChildren);
            }
            if(!predicate.test(item) && ( childList == null || childList.isEmpty()) ){
                iterator.remove();
            }
        }
        return tree;
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1969161.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux网络之多路转接——老派的select

目录 一、高级IO 1.1 概念 1.2 五种IO模型 1.3 小结 二、多路转接的老派 2.1 select 的作用 2.2 select 的接口 三、select 的编写 3.1 类的预先准备 3.2 类的整体框架 3.3 类的执行 Loop 四、Loop 中的回调函数 4.1 HandlerEvent 4.2 AcceptClient 4.3 Service…

二、4 函数的递归与迭代

1、n 的阶乘 2、斐波那契数列 &#xff08;1&#xff09;递归 用递归解决这个问题&#xff0c;由于需要多次重复计算&#xff0c;当 n 较大时&#xff0c;计算效率就非常慢 &#xff08;2&#xff09;迭代&#xff08;速度更快&#xff09;

C# 下⽀持表达式树的框架类型详解与示例

文章目录 什么是表达式树&#xff1f;表达式树的主要用途包括&#xff1a;表达式树节点类型示例&#xff1a;创建一个简单的加法表达式树示例&#xff1a;使用表达式树进行数据绑定示例&#xff1a;动态生成代码总结 在C#中&#xff0c;表达式树&#xff08;Expression Tree&am…

首届「中国可观测日」圆满落幕

首届中国可观测日&#xff08;Observability Day&#xff09;在上海圆满落幕&#xff0c;为监控观测领域带来了一场技术盛宴。作为技术交流的重要平台&#xff0c;此次活动不仅促进了观测云与亚马逊云科技之间的深化合作&#xff0c;更标志着双方共同推动行业发展的重要里程碑。…

红蓝绿三巨头集体拉胯,NVIDIA新显卡被核显秒了

最近蓝厂 intel 的瓜想必大家都已经吃上了吧&#xff1f;13-14 代中高端 CPU 大面积故障崩溃事件。 后续是 intel 官方回应&#xff0c;系微代码错误&#xff0c;请求电压较高导致的。 intel 目前给出的方案是&#xff0c;出现了问题的 CPU&#xff0c;intel 给予免费换新售后…

微信小程序之单选框

微信小程序中的单选框&#xff08;Radio&#xff09;是一个常用的输入组件&#xff0c;用于在多个选项中进行选择。常见的应用场景有性别选择、选项过滤、问卷调查等。本文将介绍小程序中单选框的特点和作用及相应示例。 一、单选框的特点和作用 特点&#xff1a; 单一选择&a…

php yii2 foreach中使用事务,事务中使用了 continue

问题描述&#xff1a;使用yii2&#xff0c;在foreach中使用事务&#xff0c;每个循环一个事务&#xff0c;在事务进行判断,然后直接continue,导致后面的循环数据没有保存成功 如下图&#xff1a; 修改后&#xff1a;如下图

【人工智能学习之商品检测实战】

【人工智能学习之商品检测实战】 1 开发过程2 网络训练效果2.1 分割网络2.2 特征网络 3 跟踪与后处理4 特征库优化5 项目源码解析5.1 yolo训练train_yolo.pygood_net.pydataset.pygood_cls_data.pysave_feature.pyanalyse_good.pyshop_window.pytest.py 6 结语 1 开发过程 拍摄…

Spring boot 整合influxdb2

一.服务安装 docker search influxdb docker pull influxdb docker run -dit --name influxdb --restart always -p 8086:8086 -v /dp/docker/file/influxdb:/var/lib/influxdb influxdb 访问8086 初始化 账号组织和新建bucket 创建密钥 这些豆记录下来 二.项目配置 引入依赖…

什么是物流锁控,RFID物流智能锁对于物流锁控有什么意义

在当今竞争激烈的全球商业环境中&#xff0c;物流行业作为经济发展的重要支撑&#xff0c;其高效、安全的运作至关重要。物流锁控作为保障物流运输过程中货物安全、准确和及时交付的关键环节&#xff0c;正面临着日益复杂的挑战。 一、物流锁控的定义与范畴 物流锁控&#xf…

JavaScript学习笔记(十一):JS Browser BOM

1、JavaScript Window - 浏览器对象模型 浏览器对象模型&#xff08;Browser Object Model (BOM)&#xff09;允许 JavaScript 与浏览器对话。 1.1 浏览器对象模型&#xff08;Browser Object Model (BOM)&#xff09; 不存在浏览器对象模型&#xff08;BOM&#xff09;的官方…

【周易哲学】生辰八字入门讲解(一)

&#x1f60a;你好&#xff0c;我是小航&#xff0c;一个正在变秃、变强的文艺倾年。 &#x1f514;本文讲解【周易哲学】生辰八字入门讲解&#xff0c;期待与你一同探索、学习、进步&#xff0c;一起卷起来叭&#xff01; 目录 生辰八字阴阳五行天干地支天干天干天干五合天干…

怎么恢复电脑删除的数据?4种有效的数据恢复方法

在数字时代&#xff0c;数据已成为我们生活与工作的核心要素&#xff0c;它不仅存储着我们工作中的各种重要资料&#xff0c;也记录着个人生活的点点滴滴。无论是精心准备的工作文档或者珍藏的重点照片&#xff0c;在不经意间被误删、因设备故障、中病毒等原因导致丢失&#xf…

AI砸掉了这些人的饭碗

在一般打工人眼里&#xff0c;金融圈往往被认为是高端脑力工作者的聚集地&#xff0c;他们工资高&#xff0c;学历高&#xff0c;能力强&#xff0c;轻易无法被替代。 可最近&#xff0c;偏偏一个“非人类”的物种&#xff0c;要来抢他们的饭碗。相关报道称&#xff0c;华尔街…

因为不懂Redis集群,我被diss了

点赞再看&#xff0c;Java进阶一大半 Redis官方在官网里写着霸气的宣传语&#xff1a;从构建者那里获取世界上最快的内存数据库Get the world’s fastest in-memory database from the ones who built it。南哥相信国内没用Redis的科技公司也屈指可数。 现在Redis已经走向了商业…

C++学习:C++是如何运行的

C 是一种强类型的编程语言&#xff0c;支持面向对象、泛型和低级内存操作。它的工作机制包括从编写源代码到生成可执行文件的一系列步骤。C与文件无关&#xff0c;文件只是容纳运行内容的载体&#xff0c;需要对文件以目标系统的规则编译后&#xff0c;才能在目标系统中运行。 …

红酒与情感:品味中的情感共鸣

在生活的细微之处&#xff0c;情感如涓涓细流&#xff0c;无声无息却又深深地影响着我们。而红酒&#xff0c;这瓶中的液体&#xff0c;仿佛是情感的载体&#xff0c;让我们在品味中感受那些难以言说的情愫。当定制红酒洒派红酒&#xff08;Bold & Generous&#xff09;与情…