前言❤️❤️
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
这篇文章基本上涵盖了在JavaSE中方法的很多重要概念,铁汁们可以分两次或者3次来看,也可以根据目录挑自己掌握薄弱的来看
除了基础概念,递归部分一些铁汁可能相对来说比较薄弱,因此文章中将近一半的内容都是在说递归,这部分的解析也比较详细
递归是很重要的一个部分,后面在数据结构的算法题,面试题中会经常遇到递归,因此现在一定要把基础给打牢💪💪💪
🎇个人主页:洋不写bug的博客
🎇所属专栏:Java学习之旅,从入门到进阶
🎇铁汁们对于Java的各种常用核心语法(不太常用的也有😆),都可以在上面的Java专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~
1,方法的简介
方法就是函数,只不过Java中没有函数这一说,(在C语言中把它叫做函数)把它叫做方法,方法也就是一段可以重复利用的代码片段。
我们引入方法/函数的原因大家也能感受到,那就是为了不把一大团代码写在主函数中,看起来特别乱,维护也很困难
而使用方法,只需在主函数中一句话,其他的执行逻辑都在方法的代码片段中,这也让程序员思考代码的负担降低了
程序员的核心工作,就是针对项目“复杂程度”的管理!!!
2,方法的定义
方法的语法格式如下:

就拿main方法来看,public static是修饰符,void是返回值类型,main是方法名称,而括号里面的,就是参数类型和形参(这里只需要了解一下即可,不必完全理解)
在写方法时,要根据方法的作用来命名,跟C语言相同
给方法命名一般使用“驼峰命名法”,那就是第一个单词的首字母小写,后面每个单词的首字母都大写,而且,不能用关键字给方法命名(比如int,public这些)。
- 给变量起名时,一般用“名词”。
- 给方法起名时,一般用“动词/动宾短语”。
在方法的括号里面,可以有一个参数,可以没有参数,也可以有多个参数,都是参数的类型+参数的变量名(这点跟C语言也基本相同)
普通的方法都要调用时才会执行,而main方法是通过JVM(Java虚拟机)来调用的
不太清楚JVM概念的铁汁可以看第一篇Java入门博客的(3,Java的特点),链接放在下面了
Java入门第一篇下面是Java中方法使用的注意事项,铁汁们应该大部分都可以看懂,部分看不懂的内容可以看完整篇博客后再回来看,应该差不多就能懂了
- 修饰符:现阶段直接使用public static固定搭配。
- 返回值类型:如果有返回值,返回值类型在函数定义时写的的必须和实际返回的一样,如果没有返回值,就写成void。
- 方法名字:采用小驼峰命名法。
- 参数列表(方法括号里写的变量):如果方法没有参数,就什么都不用写;如果有参数,不同的参数之间需要用逗号隔开。
- 方法体:方法内部要执行的语句。
- 在Java中,方法必须写在类中(这点跟C语言不同)。
- 在Java中,方法不能嵌套定义。
- 在Java中,没有方法声明这一说,不管方法在main函数前还是后,都可以直接使用。
3,闰年的判断方法
方法需求:判定一个年份是不是闰年,在主函数中输入一个年份,调用这个方法,来判断输入的年份是不是闰年。
1,方法1
import java.util.Scanner; public class exercise { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入年份:"); int year = scanner.nextInt(); if(isLeapYear(year)){ System.out.println(year + "是闰年"); }else{ System.out.println(year + "不是闰年"); } } public static boolean isLeapYear(int year){ if(year % 100 == 0){ if(year % 400 == 0){ return true; }else{ return false; } }else{ if(year % 4 == 0){ return true; }else{ return false; } } } }闰年的判断思路和原理,我们在逻辑控制那篇已经讲过,链接如下,有兴趣可以看下:
那这里重点就放在了方法的使用上:
①闰年判断方法跟main方法都写在我们创建的exercise类中
换句话说,写的方法跟main方法是同级的,不能把方法写在exercise类之外,这点是初学时容易出错,exercise就是我们创建的整体的一个大类,要在exercise这个类中写代码。
②方法的命名,判断是否是闰年,用动宾短语来命名方法,也就是动词+名词,这里的isLeapYear就是使用前面提到的 小驼峰命名法(第一个单词小写,后面每个单词都大写)
③实参和形参(定义跟C语言中几乎是一样的)
方法括号里的参数是形参,没有实际的值,只是定义一下。

在主函数中调用方法时,这里的这个year就是实参,一个实际的数据。

2,方法2
需求:让用户通过控制台输入一个整数,并打印是否是闰年。
(这个需求跟方法1的需求本质上是两码事)import java.util.Scanner; public class exercise { public static void main(String[] args) { while(true){ isLeapYear2(); } } public static void isLeapYear2(){ Scanner scanner = new Scanner(System.in); System.out.println("请输入年份:"); int year = scanner.nextInt(); if(year % 100 == 0){ if(year % 400 == 0){ System.out.println(year + "是闰年"); }else{ System.out.println(year + "不是闰年"); } }else{ if(year % 4 == 0){ System.out.println(year + "是闰年"); }else{ System.out.println(year + "不是闰年"); } } } }这个就是读取,输出操作都加入到了函数中,把函数套在main的while循环中,就能无限循环,这个函数就相对更加独立一些。
通常,第一个写法的风格会更好一些。
4,两个整数相加
1,一个一个输入
import java.util.Scanner; public class exercise { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入第一个整数"); int num1 = scanner.nextInt(); System.out.println("请输入第二个整数"); int num2 = scanner.nextInt(); int result = add(num1,num2); System.out.println("结果为:" + result); } public static int add(int num1,int num2){ return num1 + num2; } }
2,直接一起输入
连着写两个scanner.nextInt();就是连续读取两个数据
import java.util.Scanner; public class exercise { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入两个整数"); int num1 = scanner.nextInt(); int num2 = scanner.nextInt(); int result = add(num1,num2); System.out.println("结果为:" + result); } public static int add(int num1,int num2){ return num1 + num2; } }
5,方法之间的相互调用
铁汁们可以对照结果分析一下代码的执行顺序,这个很简单,我们就不再分析了。
public class exercise { public static void main(String[] args) { System.out.println("开始程序"); func1(); System.out.println("结束程序"); } public static void func1(){ System.out.println("执行func1"); func2(); System.out.println("结束func1"); } public static void func2(){ System.out.println("执行func2"); System.out.println("结束func2"); } }
理解方法相互调用,就跟盗梦空间类似,在梦中做梦,多重梦境。
6,使用方法算1-5的阶乘的和
这个具体实现再前面的逻辑控制博客的第6部分有提到(链接放在下面了),感兴趣可以看下:
逻辑控制public class exercise { public static void main(String[] args) { int sum = 0; for(int i = 1;i <= 5;i++){ sum += factor(i); } System.out.println(sum); } public static int factor(int n){ int result = 1; for(int i = 1;i <= n;i++){ result *= i; } return result; } }这里用两个for循环,计算逻辑一目了然,这就是方法的好处,将复杂的东西变得清晰简单,不容易搞混。
7,形参和实参
下面方法中括号里面的n就是形参(形式参数),如果不调用的话,这个形参就没有具体的值
而在main函数中调用func,里面的num,就是实参(实际参数),有实际的值,比如说1,2,3,4,5;
public class exercise { public static void main(String[] args) { int num = 10; func(num); System.out.println("num = " + num); } public static void func(int n){ n = 100; System.out.println("n = " + n); } }上面的程序在函数中修改了n的值,但是看下面的结果num的值并不会被改变
这里的n就相当于是num的一份拷贝,n和num存储在不同的位置,因此在函数中修改形参n,自然不会影响到main函数里的实参num.
也可以使用变量来接受函数中的返回值(前提是函数有返回值),示例如下:
public class exercise { public static void main(String[] args) { int num = func(); } public static long func(){ return 100; } }在调用函数接收返回值的时候,也要注意数据类型的转化
像下面这段代码,返回的是long类型(8个字节),而却用int类型(4个字节)的变量去接收,这种赋值肯定会报错。(大字节转小字节不能直接转)
有关数据之间赋值和类型转化,可以看之前对于数据类型变量介绍的那篇博客的第3部分
数据类型变量
这里如果想不报错的的话,可以进行强制类型转换,但是一般不建议这么做,返回值是long,就定义一个long类型的变量去接收。

8,方法重载
1,基础讲解
现在要写一个这样的程序,要求可以满足计算 2个或3个 整数或者浮点数 的相加的结果,那按照我们的理解,那就要写4个方法,就像下面这样,但是这样就特别的麻烦
public static int add2Int(int x ,int y){ return x + y; } public static int add3Int(int x ,int y, int z){ return x + y + z; } public static double add2Double(double x,double y){ return x + y; } public static double add2Double(double x,double y,double z){ return x + y + z; }那能不能这些方法的名字相同,都叫add呢?大家可以尝试一下,这样写竟然并不会报错,这是因为Java中有种叫方法重载的语法机制,可以写相同名字的方法,但是它们的参数类型不能相同
public static int add(int x ,int y){ return x + y; } public static int add(int x ,int y, int z){ return x + y + z; } public static double add(double x,double y){ return x + y; } public static double add(double x,double y,double z){ return x + y + z; }那么在调用add方法时,系统是如何判定到底调用哪个方法呢?
系统的判定是根据方法名字和里面的参数列表来选的,这里方法名字相同,那就是根据参数列表来判断,我们传入两个double类型,就进入第三个方法;当我们传3个整数时,那就是进入第二个方法。
像下面这段代码就是一个调用参数类型为int的,一个调用参数类型为double的public static void main(String[] args) { System.out.println(add(1,2)); System.out.println(add(1.1,2.2)); }多个方法是一个名字,在调用时通过参数个数和参数类型来判断调用哪个方法,我们称这个为方法重载。
2,方法二义性问题
方法二义性问题指在调用方法时,系统不知道使用哪个方法,有两个方法都满足条件,这样就会报错,在写方法时,要注意这个问题
下面这个代码,在IDEA的审核中,这个方法无论是否调用都会报错
看这里的报错信息,那就是在main函数,编译器无法区分到底调用哪个方法,进而报错。
调用方法时,到底调用哪个方法,是根据方法名和参数列表来决定的,如果方法名相同,就只看参数列表,调用时不看返回值类型的
9,方法递归基础(难点)
在C语言中,我们已经接触了方法递归,
不知道大家有没有这样一种感觉:就是方法递归代码仔细看也能看懂,但是自己写的时候也不好想,这其实就需要多写递归代码,多练习,接下来就会深度解析一些经典的使用到方法递归的代码。方法递归的特点就是:代码写的时候,可能只有很少,但是理解起来,就特别难。
1,递归求n的阶乘
这里先从最基础的方法递归来说起,下面这段代码就是通过不断在factor函数中不断地自己调用自己,来实现求一个数的阶乘。
这里大家应该可以看懂,如果看不懂的话,可以带一个小一点的数,在本子上或者电脑上用笔画一下结果。再复杂的递归函数,当你开始带入数字画过程的时候,其实已经成功了一半了💪💪💪
import java.util.Scanner; public class exercise { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个数:"); int num = scanner.nextInt(); System.out.println(num + "的阶乘是:" + factor(num)); } public static int factor(int n) { //0的阶乘也是1 if (n == 0 || n == 1) { return 1; } else { return factor(n - 1) * n; } } }这里可以带入5来看下执行过程:
- 进入facor函数,返回一个 factor(4) * 5,接着自己调用自己,调用factor(4).
- factor(4),再次自己调用自己,返回一个factor(3) * 4
- 那这时候①中返回的就是factor(3) * 4 * 5;开始调用factor(3)
- factor(3)进入函数,再次自己调用自己,变成factor(2) * 3,这时候①就变成了
- 再次自己调用自己,计算factor(2),factor(2) = factor(1) * 2
- factor(1) = 1,那这时候①中返回的就是5 * 4 * 3 * 2 * 1.
在每一层递归中,factor(n)中的n的值都不同。每一层递归,其实n都是不同的变量
这个递归再仔细分析一下,就会发现其实可以反过来看,最后一步计算出factor(1),返回到计算factor(2)的方法中,进而计算出factor(2)的值,factor(2)的值,再返回到计算factor(3)的方法中,就这样以此类推,一直到计算出factor(5)的值。
方法必须要有结束的条件,在这里当递归到factor(1)时,方法就结束递归,这就属于结束条件,如果递归时没有结束条件,那么就会死循环,最后报错。如果还是不理解的话,最好在本子上画个图,有的干想是几乎想不出来的,在本子上带入一个数画几个图,就很快可以理解
2,通过调试看方法执行时的调用逻辑
当我们不太理解一个方法时,调试也是一个很好的办法,在Java中,调试是远远要比C语言中简单的。
首先就是打断点,Java中前面会标出这里是第几行,我们点击这个数字,断点就打上了,这里就在函数的自己调用自己的地方打上一个断点,在main函数中调用函数的地方打上一个断点,其他的函数思路也基本上是这样


断点打好以后,点击IDEA界面右上角的类似蜘蛛图标,(我们把鼠标放上面,会显示这个图标的功能,就是Debug,也就是调试的意思)

还有一种方法,就是点击鼠标右键,选择debug(调试),就可以开始调试了
有时候点击右上角运行的是我们的上一个程序,这时候就必须在当前界面右键来点击debug
debug点击之后,先在控制台上输出一个数,再点击enter键,就会跳到这个框里面:

这时候再按f9(笔记本电脑可能要按fn + f9),按f9是走一步,遇到断点停止

这时候就会走到第一步这里,接着再按f9,我们会发现每按一次f9,左边这里就会多出一个,这个其实就是创建的栈帧,一个栈帧对应着一个方法的调用,最后当创建了4个栈帧时,也就是当n = 2时的方法调用,方法调用了4次,这时候再按f9,就能直接计算出结果,因为factor(1)是确定的

f9是让程序直接运行到下一个断点处,是平常调试时使用最多的,现在基本上所有情况下的调试都可以用f9解决
其他的调试按键(了解一下即可):
- F8:按这个键会执行当前的一行代码,然后将执行位置移动到下一行。如果当前行调用了方法,这个方法会被直接执行,不会进入方法内部进行调试。
- F7:按此键会进入当前调用的方法内部,从而可以对方法内部的代码进行逐行调试。
- Shift + F8:当在方法内部调试时,按这个键会直接执行完当前方法的剩余代码,然后返回到调用该方法的那一行
除此之外,在调试的时候,代码旁边也会写当前执行时变量的值,这也是IDEA的一大特色

10,递归按顺序打印数字的每一位
例如输入一个数1234,那最后就打印1 2 3 4.
这道题事先是不知道几位的,因此,如果不使用递归的话,可能很多人都会使用数组先存上 4 3 2 1,然后再按反序来逐个打印数组中的元素,但是,如果用递归的话,代码会超级简短😍😍😍import java.util.Scanner; public class exercise { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个数"); int num = scanner.nextInt(); print(num); } public static void print(int n){ if(n >= 10){ print(n / 10); } System.out.printf("%d ",n % 10); } }这里博主用计算机自带的画图工具画了个递归调用图,画的线有点歪😅😥
这里红色线就是递归的部分,绿色的箭头就是递归完成后的逐步回退操作
① 首先1234传入,判断出来大于10,因此进入下一步递归
这里很重要一个点:第一个传入1234的代码中,if语句(红色线画的)执行完,包括其递归也全部执行完,才能轮到打印代码的执行(画绿色线的部分)② 接着递归到第二段代码,传的是123,这里判断出还是大于10,因此继续递归,而下面的打印语句就不执行
接着分析下一段:

③继续递归,递归到12,判断出来12大于10,继续往下递归1。这里也是,上面if语句中的递归执行不完,就还是一直停留在if语句,不会运行下面的打印逻辑。
④递归到1以后,这时候也是终于if判断的结果为false了,那就打印1,在print(12)这段代码中,if语句中的递归完成了,开始执行下面的打印操作。
⑤:那么print(12)接下来打印2,那print(12)也算执行完了,后面就开始继续往前走(绿色箭头),那在print(123)中的if语句也算执行完了,就打印3
⑥打印完3以后,print(123)中的语句也算执行完了,也就是说print(1234)中的if语句执行完了,接着就开始打印4,那么这个递归这时候就完了,完结撒花🥳🥳
下面是完整的流程图,红色箭头表示逐步递归,绿色箭头表示递归代码执行完成后的逐步回退。这里会发现,打印的执行是反过来的,这也是递归的一个基本的特征,因此只用脑子想,不画图,理解起来可能会有些困难,但是一画图,就清楚很多了。

11,递归求1 + 2 + 3 + … + 10
1,代码
这种递归的思路就是甩锅
举个例子,我们让一个同学马上算出从1加到9不太可能,那就甩锅,找个同学算从1加到99,也就是add(9),那直接把10加上他算的数即可
算add(9)的同学再甩锅,再找个同学算add(8),自己把9 + add(8)就是结果
这样一直甩锅,直到甩到add(1),那就计算出来了public class exercise { public static void main(String[] args) { System.out.println(add(10)); } public static int add(int n){ if(n == 1){ return 1; } return n + add(n - 1); } }这个代码的递归逻辑比较简单,博主就不再画递归图了,如果不是很理解,可以自己用本子或者电脑的画图软件画一下递归图,马上就理解了
2,栈溢出讲解
前面说过,递归一定要有结束条件,否则会无限循环,那就用这道题不写结束条件,来测试一下。
public class exercise { public static void main(String[] args) { System.out.println(add(10)); } public static int add(int n){ return n + add(n - 1); } }
大家是否记得在前面9.2调试部分,我提到了递归调用方法时,会创建栈帧。
递归几次就会创建几层栈帧,每一层栈帧都会消耗一定的存储空间。
当递归无限循环时,就会创建很多的栈帧,最后把栈空间给消耗殆尽了,进而系统就会判断出错了,接着报错,结束程序。
而这道题如果是正常加上结束条件的话,就会递归10次,创建10层栈帧:

12,递归针对输入整数的每一位数求和
例如输入1234,那针对每一位进行求和后结果就是10.
import java.util.Scanner; public class exercise { public static void main(String[] args) { System.out.println("请输入一个整数:"); Scanner scanner = new Scanner(System.in); int num = scanner.nextInt(); System.out.println(sum(num)); } public static int sum(int n){ if(n < 10){ return n; //一位数 } return n % 10 + sum(n / 10); } }
这里博主再画一下递归图,就拿1234举例,跟上一道题目一样,我们用红线表示逐步递归,绿线表示递归到最后一步后的逐步往前返回:

这个比较简单,就不再细讲了,铁汁们也可以自己画递归图尝试一下🐵🐵🐵
13,求斐波那契数列的第N项
1,递归写法
斐波那契数列介绍:
当n = 1或n = 2的时候,fib(n) = 1
当n为其他值时,fib(n) = fib(n - 1) + fib(n - 2)这个代码用递归来写,代码看起来会超级简约😎😎😎
还是递归甩锅的思路,铁汁们可以参考前几道题目,画一下递归图。public class exercise { public static void main(String[] args) { System.out.println(fib(4)); } public static int fib(int n){ if(n == 1 || n == 2){ return 1; //一位数 } return fib(n - 1) + fib(n - 2); } }但是,这道题的递归写法这么简约,那就一定比非递归要好吗❓
由于斐波那契数列的递归的特征,在使用递归计算时会出现大量的重复的代码
拿计算fib(20)来举例,这里只画一层递归,就足以说明问题了,这里有三个代码段,上面是fib(20),下面两个是fib(19)和fib(18)
在fib(20)中,递归计算了一次fib(18),那在fib(19)中,也是递归计算了一次fib(18),相当于fib(18)被递归计算了两次
同时,我们也能看出,在fib(19)和fib(18)中,都递归计算出了fib(17),相当于fib(17)就被递归计算两次这里还只是画了一层,就有2个数据被重复计算
那么如果递归个几十层,重复的递归会有很多很多,这些大量重复计算,就会大大降低运行效率
因此,这个代码递归写法的效率是很低的,铁汁们可以自己在IDEA上试一下,当求fib(100)的时候,可能从开始运行到在控制台上显示出结果需要很长时间
因此,有时候不一定代码简约,行数少,就是好代码,求斐波那契数列,还是比较推荐非递归的写法
2,非递归写法(循环)
目前一个比较好的写法就是循环,铁汁们在C语言里应该也学过,循环写法没有重复计算,比递归的效率要高的太多太多了。
因为第一项和第二项都是1,因此for循环就是直接从第三项开始。
这里last2是所求数前两个数的第一项,last1是所求数前两个数的第二项。
因此for循环一次,把上一次计算的cur赋值给last1(前两项的第二项),last1的值赋值给last2(前两项的第一项),这样就往后面顺延了一个斐波那契数
如果不理解,可以画一下过程public class exercise { public static void main(String[] args) { System.out.println(fib(4)); } public static int fib(int n){ if(n == 1 || n == 2){ return 1; //一位数 } int last2 = 1;//last是上一次的意思,last2就是前面两个,也就是所求数前面两个数的第一个 int last1 = 1;//last1就是前面一个,也就是所求数前面两个数的第二个 //我们分析一下可以得出,last1是要大于last2的 int cur = 0;//当前项 for(int i = 3;i <= n;i++){ cur = last1 + last2; //更新前两项 last2 = last1; last1 = cur; } return cur; } }14,使用递归的建议
递归的代码,看起来好像不是很多,但是执行的过程通常就比较复杂,实际工作中,一般不建议使用递归(除非这段代码特别适合递归)。
递归不好把握,执行过程会很复杂,一旦出现问题,也不好进行调试,而且,递归的代码,可能只是看上去简短一些,但是执行效率却往往不如非递归的代码,因为方法的不断调用,会不断创建栈帧,这本身就是一笔不小的开销。
我们在工作中写的代码,要有两种特征。
代码简单,好理解,好维护 => 提高开发效率
要么代码执行效率高 => 提升运行效率但是根据递归的特点,往往会两头都不占
但是在一些笔试面试题中,这类问题还是挺多的,这些算法题只是考察我们的知识储备和逻辑,大部分面试题,其实是跟工作场景没什么关系的😅
结语💕💕
方法部分跟C语言的函数是很相似的,方法重载对初学者来说是一个新的概念,一定要多加练习
方法递归是方法中特别重要的一部分,如果不理解,很多递归方法光想是不好理解的,一定不要嫌麻烦,画个递归图,看下逻辑,能解决很多复杂的递归问题
递归不能嫌麻烦,要投入时间来学习,在数据结构和面试题中,会有不少题目使用递归思路,因此先在一定要打好基础💪💪💪🥳🥳大家都学废了吗?完结撒花~ 🎉🎉🎉



938

被折叠的 条评论
为什么被折叠?



