本文来自李明子csdn博客(http://blog.csdn.net/free1985),商业转载请联系博主获得授权,非商业转载请注明出处!
1 引子
递归,是我们在日常开发过程中经常会使用的技术。它广泛的被现代开发语言所支持。然而,在日常的管理和招聘工作中,我发现由于开发者个人经验不足及团队开发规范不明确等原因,很多开发者不知道应该何时编写递归函数,怎么编写递归函数。一些开发者编写的递归函数存在可读性差、调试困难、逻辑混乱等问题,甚至还会引入死循环等后果严重的bug。我将在本文中带领读者回顾递归的基础知识,剖析递归函数的设计要点并分享一些相关的最佳实践。
2 概念回顾
递归在维基百科的解释如下:递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。
简单的说,对于开发者而言,递归函数就是一个在函数中调用自己的函数。比如,下面是一个计算阶乘的递归函数。
/**
* 计算阶乘
* @param num 欲计算阶乘的数值
* @return 阶乘的值
*/
static public int factorial(int num){
if(num==0){
return 1;
}else{
return num*factorial(num-1);
}
}
该函数的逻辑非常简单,如果当前值为0则返回1,否则返回当前值与“当前值减一”的乘积。
3 使用递归函数的场景
本节我将介绍递归函数的几种常见使用场景。需要指出的是,在这些常见场景中,递归并不是唯一的解决方案,在实际工作中,需要读者具体情况具体分析,因地制宜的做出更贴合实际的设计。
3.1 线性逼近
线性逼近是递归函数最原始也是最常见的应用场景。比如前边提到的计算阶乘的例子,就是典型的线性逼近——当前值的计算依赖于的对逼近直接结果的另一个值的计算。这样,只有逐渐沿着变化趋势线性的找到可以计算出直接结果的值才能逆向返回,最终得到目标结果。线性逼近这类场景,每次处理的对象均不同,但从业务含义上,它们是存在一定变化趋势的“同级”对象,所以叫做线性逼近。
3.2 重试
对于这类场景,函数不停的执行同一业务操作,直到满足一定条件(通常是达到业务目的)。这种递归函数的结构看起来更像是一个while循环。比如,我们在进行数据库连接时,可以编写如下的递归函数:
/**
* 获取数据库连接
* @param url 数据库连接串
* @param maxRetryNum 最大重试数
* @return 数据库连接
*/
public Connection getConnection(String url,int maxRetryNum){
if(maxRetryNum<1){
return null;
}
try {
// 数据库连接
Connection conn = DriverManager.getConnection(url);
return conn;
} catch (SQLException e) {
return getConnection(url,--maxRetryNum);
}
}
上边代码中的递归函数有一个表示最大重试次数的参数maxRetryNum。当获取数据库连接失败时,它将通过递归不停重试,除非超过最大重试次数。
可以看到,对于重试类型的递归函数,每次调用处理的都是相同对象的相同业务。
3.3 树状结构遍历
在日常开发中,经常会遇到树状结构,比如组织机构树、功能树等。我们常常需要按照某个顺序遍历树状结构的某个分支的各个节点。这里说的“某个顺序”一般为自顶向下(指定节点及其子孙节点)和自底向上(指定节点及其祖先节点)。对这些符合条件的节点,我们会做一些操作,比如匹配查找、设置状态等。此时,我们可以使用递归函数来实现遍历。比如下面是一个对组织结构树进行遍历处理的示例:
// 组织机构
class Org {
/**
* 构造函数
*
* @param name 组织名称
*/
public Org(String name) {
this.name = name;
}
// 组织名
public String name;
// 子组织
public List<Org> childrenOrg = new ArrayList<Org>();
/**
* 选中组织
*/
public void select() {
System.out.println(name + " is selected");
}
}
/**
* 选择组织及其子孙组织
*
* @param org 要选择的组织
*/
public void selectOrgAndDescendants (Org org) {
org.Select();
for (Org childOrg : org.childrenOrg) {
selectOrgAndDescendants (childOrg);
}
}
结合上面的代码可以看到,对于树状结构遍历类型的递归,每次调用递归函数都会首先处理当前节点,之后遍历当前节点的子节点,为每个子节点调用递归函数,做相同的业务处理。
3.4 监听
模块、系统间的协同会用到一种监听策略。监听主体根据既定规则(如时间间隔)向被监听对象询问(或直接查询)状态,判断被监听对象状态,进而做出某个动作。下面介绍监听的几种典型的应用场景。
守护进程。当进程B发现自己守护的进程A被关闭后立刻启动进程A使其得以恢复运行。
心跳监测及双机保障。当副服务器B发现主服务器A故障或失联后,服务器B马上将自己的工作模式切换为主服务器,接管服务器A的工作,并向管理员发出通知,处理服务器A的故障。
资源处理。这里提到的资源可能是一组存放在FTP服务器中指定目录下的文件,也可能是内存中的一个链表。当监听线程发现资源池不为空(即有待处理的资源)时,就将资源按照一定的顺序一一摘取,执行预定操作。比如对FTP服务器中指定目录下的用户上传文件做杀毒、压缩、归档处理。
上面这些典型的监听场景都可以通过递归函数来实现,我们来看示例代码。
/**
* 监听
*/
public void monitor(){
// 符合退出条件
if(hasSystemExited){
return;
}
// 被监听对象状态
State objectState = getMonitoredObjectState();
// 被监听对象状态符合某条件
if (objectState.hasChanged()) {
// 对被监听对象执行某操作
doSomething();
}else{
// 休眠500毫秒
sleep(500);
}
monitor();
}
在上面的示例代码中,递归函数monitor对被监听对象的状态进行监控。如果被监听对象的状态符合某个条件则对被监听对象执行某操作,否则休眠500毫秒。之后再次对自身调用。我们可以看到,这种类型的递归函数形态与while循环十分相似。
(未完待续)