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

java · 浏览次数 : 0

小编点评

【进阶篇】Java 项目中对使用递归的理解分享 前言 在Java项目中,递归是一种非常强大的解决问题的方法。本文将探讨递归的基本概念、实际案例以及改进方案。 一、什么是递归 1.1 基本概念 递归是指在方法的定义中调用自身的过程。递归是基于方法调用栈的原理实现的。当一个方法被调用时,会在调用栈中创建一个对应的栈帧,包含方法的参数、局部变量和返回地址等信息。在递归中,方法会在自身的定义中调用自身,这会导致多个相同方法的栈帧依次入栈。当满足终止条件时,递归开始回溯,栈帧依次出栈,方法得以执行完毕。递归的关键是定义好递归的终止条件和递归调用的条件。 1.2 优缺点 优点:简化问题、实现高效算法。 缺点:栈溢出风险、性能损耗、可读性不高。 1.3 与迭代的区别 迭代常见于 for 循环中,通过设置 JVM 参数来控制递归的最大深度,或者采用深度优先遍历等方法避免栈溢出。 二、实际案例 以递归获取某个评论id下面所有的子级评论id为例子,向大家介绍这个递归的过程。 首先,给出一个简单的数据库评论表的 demo,id 是主键id 也是评论唯一 id,parent_id 是该条评论的父评论 id,status 为1表示审核通过的状态。 然后,分析如何将21、28、29、31、32都放进一个集合里返回。 最后,通过脱敏处理后的代码示例,展示了递归的过程。 三、改进方案 根据个人开发经验,可以从控制递归层数和改用 Stream 这两种办法来对递归进行改进。 3.1 控制递归层数 可以通过设置 JVM 参数来控制递归的最大深度,或者采用深度优先遍历等方法避免栈溢出。 3.2 用 Stream 遍历 核心思路是:先数据库全量查询(10万条以内),内存中使用 Stream 流操作、Lambda 表达式、Java 地址引用进行筛选。适用于数据总量不多的情况。 四、文章小结 合理使用递归可以成为解决特定问题的一个利器,但需要注意控制递归层数以避免栈溢出等问题。本文仅供参考,如有不足和错误,欢迎指正和交流。

正文

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

前言

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

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

一、什么是递归

1.1基本概念

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

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

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

1.2优缺点

优点:

  1. 简化问题:递归能够将复杂问题分解成更小规模的子问题,简化了问题的解决过程;
  2. 实现高效算法:递归在某些算法中能够实现高效的解决方法,如数据结构操作中遍历树节点等。

缺点:

  1. 栈溢出风险:递归可能导致方法调用栈过深,造成栈内存溢出;
  2. 性能损耗:递归调用需要创建多个栈帧,对系统资源有一定的消耗;
  3. 可读性不高:递归的使用需要谨慎,不合理地使用可能造成代码难以理解和调试。

1.3与迭代的区别

  • 迭代(Iteration)

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

迭代示例简图
    @Test
    public 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 地址引用进行筛选。

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

详情可以看我这篇文章:https://www.cnblogs.com/CodeBlogMan/p/17965824


四、文章小结

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

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

与【进阶篇】Java 项目中对使用递归的理解分享相似的内容:

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

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

使用Java统计gitlab代码行数

一、背景: 需要对当前公司所有的项目进行代码行数的统计 二、 可实现方式 1.脚本:通过git脚本将所有的项目拉下来并然后通过进行代码行数的统计 样例: echo 创建项目对应的文件夹 mkdir 项目名称echo 切到创建的文件夹中 cd 项目名称echo 进行git初始化 git init ec

Bond——大数据时代的数据交换和存储格式

设想我们在一家很大的互联网公司做IT方面的规划、开发和维护,有以下这样的应用场景: 公司里有若干个不同的开发团队,开发语言有Java、.net、Python、C++....十来种,还有很多外包团队对项目进行开发,大中小系统已经多的数不过来;并且各个团队、系统间都需要进行海量数据的交换(比如搜索引擎实

[转帖]Core dump实战分析之Java版

https://www.jianshu.com/p/2cdf71f99209 Core dump实战分析(Java版) 背景 项目中的battleserver进程在某一段时间总是crash,无法找到具体Crash原因 Java通过JNI调用Luajit 那么进程Crash如何找到JNI的堆栈(C层)

集成Unity3D到iOS应用程序中

如果想让原生平台(例如 Java/Android、Objective C/iOS 或 Windows Win32/UWP)包含 Unity 功能,可以通过Unity 生成UnityFramework静态库包含到项目中进行实现。 Unity 从2019.3 开始支持将 Unity 运行时组件集成到原生

为什么不推荐使用Linq?

相信很多.NETer看了标题,都会忍不住好奇,点进来看看,并且顺便准备要喷作者! 这里,首先要申明一下,作者本人也非常喜欢Linq,也在各个项目中常用Linq。 我爱Linq,Linq优雅万岁!!!(PS:顺便吐槽一下,隔壁Java从8.0版本推出的Streams API,抄了个四不像,一点都不优雅

salesforce零基础学习(一百二十四)Postman 使用

本篇参考: Salesforce 集成篇零基础学习(一)Connected App salesforce 零基础学习(三十三)通过REST方式访问外部数据以及JAVA通过rest方式访问salesforce 我们在项目中也经常遇见下游系统去和我们进行交互的情况,针对 salesforce可以提供 标

Java21上手体验-分代ZGC和虚拟线程

一、导语 几天前Oracle刚刚发布了Java21, 由于这是最新的LTS版本,引起了大家的关注。 我也第一时间在个人项目中进行了升级体验。 一探究竟,和大家分享。 二、Java21更新内容介绍 官方release公告: https://jdk.java.net/21/release-notes 开

一次典型的Memroy Leak的跟踪学习过程

背景 周四时某项目在QQ群里说自己的系统出现了CPU占用较高的情况. TOP 查看发现大部分占用CPU的都是 JAVA核心进城后附近的进程. 所以初步怀疑 是出现了FullGC的问题. 然后群里反馈了dump 以及 tracelog等内容 进行了简单的分析, 这里总结一下, 备忘 关于GC进程 ja

端口占用,无法通过netstat找到进程,占用的端口又不能修改,该怎么办?

最近遇到一个奇葩的问题,项目跑的好好的,没有安装其它特殊软件,突然服务器启动报错,日志如下,显然是服务器的8080端口占用了。 Caused by: java.net.BindException: Address already in use: bind at sun.nio.ch.Net.bind