【进阶篇】Java 项目中对使用递归的理解分享

前言

笔者在最近的项目开发中,遇到了两个父子关系紧密相关的场景:评论树结构、部门树结构。具体的需求如:找出某条评论下的所有子评论id集合,找出某个部门下所有的子部门id集合。

在之前的项目开发经验中,递归使用得是较少的,但作为一个在数据结构操作中遍历树节点的解决方案,我还是拿出来作为技术积累进行记录以及分享。

一、什么是递归

1.1基本概念

这里就有必要简单介绍一下关于递归的基本概念了。

在 Java 中,递归是指在方法的定义中调用自身的过程,递归是基于方法调用栈的原理实现的:当一个方法被调用时,会在调用栈中创建一个对应的栈帧,包含方法的参数、局部变量和返回地址等信息。在递归中,方法会在自身的定义中调用自身,这会导致多个相同方法的栈帧依次入栈。当满足终止条件时,递归开始回溯,栈帧依次出栈,方法得以执行完毕。

递归的关键是定义好递归的终止条件和递归调用的条件。如果没有适当的终止条件或递归调用的条件不满足,递归可能会陷入无限循环,导致栈内存溢出。

1.2优缺点

优点:

  1. 简化问题:递归能够将复杂问题分解成更小规模的子问题,简化了问题的解决过程;

  2. 实现高效算法:递归在某些算法中能够实现高效的解决方法,如数据结构操作中遍历树节点等。

缺点:

  1. 栈溢出风险:递归可能导致方法调用栈过深,造成栈内存溢出;

  2. 性能损耗:递归调用需要创建多个栈帧,对系统资源有一定的消耗;

  3. 可读性不高:递归的使用需要谨慎,不合理地使用可能造成代码难以理解和调试。

1.3与迭代的区别

  • 迭代(Iteration)

  • 迭代常见于 for 循环中:比如有一个集合 A,对 A 进行 foreach,在内部设置条件,符合条件后将集合中某个元素的值替换成别的值。

迭代示例简图

    @Testpublic void iterationTest(){ArrayList<String> list = new ArrayList<>();list.add("计算机技术");list.add("土木工程");list.add("市场营销");list.forEach(val -> {if (val.contains("计算机")){log.info("迭代前的的专业名称:{}", val);String str = val.replace(val, "计算机科学与技术");log.info("迭代后的的专业名称:{}", str);}});}

结果为:

迭代结果简图

  • 递归(Recursion)

递归的例子会在下一小节详细给出。


二、实际案例

下面笔者以递归获取某个评论id下面所有的子级评论id为例子,向大家介绍这个递归的过程。

首先,这里给出一个简单的数据库评论表的 demo,id 是主键id 也是评论唯一 id,parent_id 是该条评论的父评论 id,status 为1表示审核通过的状态。

其中,我们可以简单发现:这里21为第一层,28和29为第二层、31和32为第三层,草图如下所示:

评论id简单层级示意图

那么,我们如何将21、28、29、31、32都放进一个集合里返回呢?下面的代码示例可以给你一个参考。

但是,在看代码之前,有个问题请你思考一下:

从21开始后,遍历的路线是21-28-29?还是21-28-31?还是21-29-32?或者是21-28-31-29-32?

下面是经过脱敏处理后的参看代码示例,注释都写得比较清楚了:

    /*** 这里可以看作是外部接口的调用,会得到递归的结果* @param id*/private List<Integer> getIdListMethod(Integer id){ArrayList<Integer> idList = new ArrayList<>();this.getAllIdByRecursion(id, idList);log.info("递归后得到的id集合:{}", idList);return idList;}/*** 这里是递归的过程* @param id* @param idList*/private void getAllIdByRecursion(Integer id, List<Integer> idList){LambdaQueryWrapper<Comment> wrapper = new LambdaQueryWrapper<>();//先把该id下所有的第一级子id找到wrapper.eq(Comment::getParentId, id).eq(Comment::getStatus, NumberUtils.INTEGER_ONE);List<Comment> commentList = this.list(wrapper);for (Comment children : commentList){this.getAllIdByRecursion(children.getId(), idList);}log.info("放入集合的id为:{}", id);idList.add(id);}

上面问题的答案是:递归后得到的id集合:[21,28,31,29,32],原因就是:迭代会从一棵树开始遍历到底,没有元素了再从头开始遍历,依次迭代,类似于深度优先遍历。

比如:21下面有两个子id:28和29,那么会先走21-28-31这棵树,到底了后接着按照29-32遍历。


三、改进方案

我根据自己的开发经验,可以从控制递归层数和改用 Stream 这两种办法来对递归进行改进。

3.1控制递归层数

JVM 默认控制的递归最大深度限制在 1000 层,可以通过设置 JVM 参数来控制其深度,如:

java -Xss5m #表示将每个线程的栈内存大小设置为5MB,已经是比较大了

或者在代码层面对递归的层数进行控制:

        int depth = 0;//递归方法调用for (int i = 0; i < 20; i++) {depth++;}if (depth > 100){//其它操作}

3.1用 Stream 遍历

核心思路是:先数据库全量查询(10万条以内),内存中使用 Stream 流操作、Lambda 表达式、Java 地址引用进行筛选。

适用于数据总量不多的情况,如:部门树,部门数量一般情况是比较固定的,一个组织或者公司最多也就几百上千个部门。


四、文章小结

笔者确实不推荐在项目中过度使用递归,但是合理使用的话也能成为解决特定问题的一个利器,至于怎么拿捏这个度,那就要看大家的具体情况了。

Java 项目中对使用递归的理解分享到这里就结束了,文章如有不足和错误,或者你有更好的解决思路,欢迎大家的指正和交流!

文章转载自:CodeBlogMan

原文链接:https://www.cnblogs.com/CodeBlogMan/p/18180395

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

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

相关文章

centos7安装python3.10

文章目录 1. 安装依赖项2. 下载Python 3.10源码3. 解压源码并进入目录4. 配置安装选项5. 编译并安装Python6. 验证安装7.创建软连接8. 安装pip39. 换源 1. 安装依赖项 sudo yum groupinstall -y "Development Tools" sudo yum install -y openssl-devel bzip2-devel…

Eureka的自扩展之道:服务自动扩展的秘诀

&#x1f31f; Eureka的自扩展之道&#xff1a;服务自动扩展的秘诀 在微服务架构中&#xff0c;服务的自动扩展是实现高可用性和弹性伸缩的关键。Eureka作为Netflix开源的服务发现框架&#xff0c;提供了一套机制来支持服务的自动扩展。本文将详细介绍Eureka如何实现服务的自动…

【LeetCode】十、二分查找法:寻找峰值 + 二维矩阵的搜索

文章目录 1、二分查找法 Binary Search2、leetcode704&#xff1a;二分查找3、leetcode35&#xff1a;搜索插入位置4、leetcode162&#xff1a;寻找峰值5、leetcode74&#xff1a;搜索二维矩阵 1、二分查找法 Binary Search 找一个数&#xff0c;有序的情况下&#xff0c;直接…

第4章:Electron主窗口与子窗口管理

4.1 创建主窗口 主窗口是 Electron 应用启动后显示的第一个窗口&#xff0c;通常用来承载应用的主界面。我们使用 BrowserWindow 类来创建主窗口。 4.1.1 创建主窗口的基础代码 // 引入 Electron 模块和 Node.js 的 path 模块 const { app, BrowserWindow } require(electr…

【动态规划 前缀和】2478. 完美分割的方案数

本文涉及知识点 划分型dp 动态规划汇总 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 LeetCode 2478. 完美分割的方案数 给你一个字符串 s &#xff0c;每个字符是数字 ‘1’ 到 ‘9’ &#xff0c;再给你两个整数 k 和 minLength 。 如…

【C++ Primer Plus学习记录】指针和const

可以用两种不同的方式将const关键字用于指针。第一种方法是让指针指向一个常量对象&#xff0c;这样就可以防止使用该指针来修改所指向的值&#xff0c;第二种方法是将指针本身声明为常量&#xff0c;这样可以防止改变指针指向的位置。 首先&#xff0c;声明一个指向常量的指针…

前后端防重复提交(续)

前文介绍过前后端防重复提交的基本场景&#xff0c;简单的情况是只发起一个异步请求&#xff0c;如果有多个异步请求怎么操作呢&#xff1f;这个要分情况看下。 如果是后端服务器的接口支持一次传递多个申请&#xff0c;那么可以将任务放进数组中&#xff0c;发往后端。这是最好…

074、Python 关于实例方法、静态方法和类方法

在Python中&#xff0c;类可以定义三种类型的方法&#xff1a;实例方法、静态方法和类方法。每种方法都有其特定的用途和调用方式。 实例方法&#xff08;Instance Methods&#xff09; 定义&#xff1a;实例方法是绑定到类实例上的方法。它们必须有一个名为self的隐式第一个参…

golang 1.22特性之for loop

背景 go1.22版本 for loop每轮循环都生成新的变量. 原谅: https://tip.golang.org/doc/go1.22 Previously, the variables declared by a “for” loop were created once and updated by each iteration. In Go 1.22, each iteration of the loop creates new variables, to …

【C++11】自己封装RAII类,有哪些坑点?带你了解移动语义的真相

文章目录 一、持有资源的类定义移动构造函数的要点1.普通内置类型与std::move2.常见的容器与std::move3.结构体&#xff1a;4.智能指针与std::move 参考 一、持有资源的类定义移动构造函数的要点 1.普通内置类型与std::move 在C中&#xff0c;std::move 主要用于对象的移动语…

Wireshark - tshark支持iptables提供数据包

tshark现在的数据包获取方式有两种&#xff0c;分别是读文件、网口监听&#xff08;af-packet原始套接字&#xff09;。两种方式在包获取上&#xff0c;都是通过读文件的形式&#xff1b;存在文件io操作&#xff0c;在专门处理大流量的情境下&#xff0c; 我们复用wireshark去做…

Windows编程上

Windows编程[上] 一、Windows API1.控制台大小设置1.1 GetStdHandle1.2 SetConsoleWindowInfo1.3 SetConsoleScreenBufferSize1.4 SetConsoleTitle1.5 封装为Innks 2.控制台字体设置以及光标调整2.1 GetConsoleCursorInfo2.2 SetConsoleCursorPosition2.3 GetCurrentConsoleFon…

python如何输出list

直接输出list_a中的元素三种方法&#xff1a; list_a [1,2,3,313,1] 第一种 for i in range(len(list_a)):print(list_a[i]) 1 2 3 313 1 第二种 for i in list_a:print(i) 1 2 3 313 1 第三种&#xff0c;使用enumerate输出list_a方法&#xff1a; for i&#xff0c;j in enum…

Redis的使用(二)redis的命令总结

1.概述 这一小节&#xff0c;我们主要来研究一下redis的五大类型的基本使用&#xff0c;数据类型如下&#xff1a; redis我们接下来看一看这八种类型的基本使用。我们可以在redis的官网查询这些命令:Commands | Docs,同时我们也可以用help 数据类型查看命令的帮助文档。 2. 常…

数据结构 - C/C++ - 串

字符处理 C 特性 C语言中字符串存储在字符数组中&#xff0c;以空字符\0结束。 字符串常量&#xff0c;const char* str "Hello"&#xff0c;存储在只读的数据段中。 布局 字符串在内存中是字符连续存储的集合&#xff0c;最后一个字符为空字符(ASCII值为0)&…

opencascade AIS_InteractiveContext源码学习7 debug visualization

AIS_InteractiveContext 前言 交互上下文&#xff08;Interactive Context&#xff09;允许您在一个或多个视图器中管理交互对象的图形行为和选择。类方法使这一操作非常透明。需要记住的是&#xff0c;对于已经被交互上下文识别的交互对象&#xff0c;必须使用上下文方法进行…

【问题已解决】Vue管理后台,点击登录按钮,会发起两次网络请求(竟然是vscode Compile Hero编译插件导致的)

问题 VueElement UI 做的管理后台&#xff0c;点击登录按钮&#xff0c;发现 接口会连续掉两次&#xff0c;发起两次网络请求&#xff0c;但其他接口都是正常调用的&#xff0c;没有这个问题&#xff0c;并且登录按钮也加了loading&#xff0c;防止重复点击&#xff0c;于是开…

搜索引擎常用语法

引号 (" "): 用双引号将词组括起来&#xff0c;搜索引擎将返回包含完全相同短语的结果。 示例&#xff1a;"人工智能发展趋势" 减号 (-): 在关键词前加上减号可以排除包含特定词语的结果。 示例&#xff1a;人工智能 -机器学习&#xff08;排除包含 “机器…

朴素贝叶斯解密:sklearn中的分类器工作原理

&#x1f4da; 朴素贝叶斯解密&#xff1a;sklearn中的分类器工作原理 在机器学习领域&#xff0c;朴素贝叶斯分类器因其简单、高效而广受欢迎。特别是在处理大量特征数据时&#xff0c;朴素贝叶斯表现出了卓越的性能。scikit-learn&#xff08;简称sklearn&#xff09;是Pyth…

JavaMySQL 学习(基础)

目录 Java CMD Java发展 计算机存储规则 Java学习 switch新用法&#xff08;可以当做if来使用&#xff09; 数组定义 随机数 Java内存分配 MySQL MySQL概述 启动和停止 客户端连接 数据模型 关系型数据库 SQL SQL通用语法 SQL分类 DDL--数据定义语言 数据库…