1、方法的简述和概念
1.1 什么是方法?
可能学过C语言的小伙伴就知道,C程序是由许许多多的函数组成的,而每个函数对应着一个功能,比如很多的一些库函数,还有我们自己会写一些函数,其实在Java中,我们把函数称作方法,如果从广义上讲,就是来帮助我们解决问题的,既然是解决问题,我们也可以把所有函数都写在main函数内部啊,那如果我们其他地方也需要呢?复制粘贴?这样太麻烦了,这里会造成大量的重复代码,增加了重复性工作,使得程序变得更繁琐,更不利于我们的维护,同时也不利于我们复用,所以为了解决这些问题,我们就出现了方法,当我们需要的时候,调用即可。
如果你还在纠结方法的好处有哪些?那请把我上面列举不使用的方法的缺点反过来,你就知道方法的好处了
这里举个例子,假设我要判断 n 的阶乘,我在main函数写了他的实现,在很下面很下面,我又需要求阶乘了,我又得去拷贝上面的求阶乘代码,还得改一下参数,这样行不行,可行!但是不方便,难不成我一个程序需要求10个数的阶乘,我有10段重复代码,那给阅读者也带来很不好的体验,接下来,我们正式进入方法的学习:
1.2 方法的定义和使用
我们就来写一个求阶乘的方法,首先我们需要知道方法的语法格式是什么:
可能看到上面这些术语的时候还有很多小伙伴不懂,什么是修饰符,这里我们需要注意如下点:
- 修饰符:在目前我们用 public static 搭配即可,当后期讲到的时候在细说
- 返回类型:如果没有返回值,可以必须写成 void,如果有,返回的类型必须与实体类型一致
- 方法名称:统一采用小驼峰命名,不要使用拼英,不要过度缩写,不要使用中文!
- 参数列表:如果不需要参数则不写,如果有参数,一定要写明参数类型,多个参数之间用逗号隔开
- 方法体:方法需要执行的语句,遇到 return 则返回
- 方法必须写在类中,方法不能嵌套定义,没有方法声明的说法
- 方法只有被调用的时候才执行,结束后回到主调方法继续往下执行
当我们拿到求阶乘的值,我们首先应该思考,阶乘是指 1 * 2 * 3 .... * n,这求的就是 n 的阶乘,首先这个函数肯定有返回值,需要产生 1~n 个连续的整数,再者我们需要拿一个变量每次把乘积存起来,这个变量的初始值不能为0, 有了这种思路,就可以开始写代码了:
那么我们写这样一个方法有什么好处呢?如果我们后续代码还需要求阶乘,是不是就可以直接调用这个函数传参即可?不仅增加了代码可读性,还更利于我们的操作。
1.3 形参与实参的关系
这里如果我们要写一个方法交换两个变量的值,于是聪明的张三就赶忙敲起键盘,不到 30 秒写完了以下代码:
public class TestDemo { public static void main(String[] args) { int a = 10; int b = 20; System.out.println("交换前:" + a + " " + b); swap(a, b); System.out.println("交换前:" + a + " " + b); } public static void swap(int x, int y) { int tmp = x; x = y; y = tmp; } }
可是运行起来发现并没有交换,如果有学习过C语言的小伙伴可能就明白了,形参并不会改变实参的值,为什么呢?因为形参只是实参的一份临时拷贝,那我们传地址,拿指针变量接收,可以吗?不可以!Java中没有所谓的指针。那我们如何解决呢,这里我们先不解决,等到后面学习引用类型,问再来拿这个例子出来修改,如果现在解决了,你也会马马虎虎的弄不懂。
形参呢只是在方法定义的时候,需要借助的变量而已,本质上是用来保存方法在调用时传过来的值,在Java中,实参的值永远都是拷贝到形参当中,本质上他们是两个实体。
什么是形参和实参呢?实参就是在调用方法时方法名括号后面中的参数,形参就是来源于方法的调用,可以理解他是实参的临时拷贝,在方法调用结束,栈帧空间会销毁。
1.4 方法的返回值
- 如果方法没有返回值,是不能拿变量去接收返回值的!也不能链式访问,否则会报错!
- 如果方法有返回值,并且方法中有分支情况,我们需要保证每个分支对应都有返回值!
具体细节我们会随着学习的深入而感受到,这里只是让你知道如何使用,在后续的学习中,等你有了一定的代码量,这其中的理论你都能明白。
2、方法重载
2.1 为什么需要方法重载?
在C语言中,我们经常写过求两个数和的函数,通常我们会写成 int 类型参数,导致于这个函数,只能进行整数求和,如果我们要进行浮点数求和,那只能在取一个名字,改成 double 类型参数,如果我们要进行三个数的求和?那我们需要写多少个函数啊?取名字也成了一件头疼的事情,可想而知,在以后的项目中,我们碰到的情况可能会更复杂,那我们可以不可以让他们都是一个函数名呢?显然C语言中是不可以的,但是Java中有了方法重载的概念,我们往后看:
2.2 方法重载的使用
在语言中,如果我们想让相同的方法名,但是可以实现不同的功能,就可以用到方法重载了,在Java中,如果多个方法的名字相同,参数列表不同,则称之为该几种方法被重载了
这里我们就来实现一下上面说的,一个 add方法名 实现两个整型相加,两个浮点数相加,三个整型相加:
在使用方法重载的时候,我们是一定要注意几个点的:
- 方法名必须相同
- 返回值类型无所谓
- 方法形参列表必须不同(顺序不同,个数不同,类型不同)
如果你让方法重载,你仅仅是因为返回值的类型不同的话,就比如你本来是 int类型 返回值,你直接改成 double 类型返回值,这样是不能构成方法重载的,那这里又有小伙伴有疑问了,那编译器怎么知道我使用的哪个方法呢?它会根据我们的实参类型进行推演,来判断我们使用的是哪个方法。具体还需要下来多多尝试写代码,才能理解更深刻。
2.3 方法的签名
在有上面的学习之后,可能很多小伙伴有疑问,我之前在C语言中写函数的时候,不能出现命名一样的函数,编译器会告诉我函数重定义了,那为什么Java中方法重载后可以同名呢?其实本质上的名字是不一样的,方法签名就是编译器编译之后修改方法的最终名字,具体的方式:方法路径全名+方法参数列表类型+返回值类型,这样才能构成完整的方法名。
public class TestDemo { public static void main(String[] args) { System.out.println(add(1, 2)); System.out.println(add(1, 2, 3)); System.out.println(add(1.2, 2.3)); } 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; } }
如果证明我们上面的结论呢?在代码如上代码编译之后,会生成字节码文件,我们可以进入生成字节码文件(.class)所处的文件夹,从地址栏输入cmd命令进入控制台,接着执行我们 javap -v 字节码文件名 就可以查看了:
方法的特殊字符说明:
3、方法的递归
递归其实在方法执行的过程中不断的自己调用自己,既然是递归,从字面意思看,他就有两个动作,一个是递,一个是归,如果一直递的话,不往回归的话,结果可想而知,是会造成无限递下去,最终导致栈溢出,为什么一直递归下去会导致栈溢出呢?
调用方法时是在JVM栈内存上开辟空间的,方法的递归,本质也是调用方法,如果要调用方法,就会形成栈帧,栈的空间是有限的,如果你一直形成新的栈帧,总会导致栈空间耗尽的情况,这就是栈溢出。
栈溢出错误
所以由上我们就能得出一个结论:递归肯定不能无限递归下去,一定要有一个递归终止条件,也就是递归的出口。
那么我们如何使用递归解决问题呢?
我们首先得了解方法的调用过程,这个在上面已经说过了,在者我们要找到递推公式,就好比我们求 5!一样,5!可以分为 5 * (5 - 1)!,那么 (5-1)!也就是 4!也可以分成 (4 - 1)!这样重复下去,我们也就产生了一个递推公式:N!= N*(N-1)!
有了思路,我们如何实现代码呢?那么终止递归条件是什么呢?当我们发现 N 的值为 1 了,也就是 1!自然也是 1,这个时候我们就可以直接 return n;代码实现如下:
public class TestDemo { public static void main(String[] args) { System.out.println(factorial(5)); } public static int factorial(int n) { if (1 == n) { return n; } return n * factorial(n - 1); } }
为了方便大家更好的理解,我们画递归展开图,3的阶乘:
对于递归来说,需要多写代码,多做练习,像我们后续Java数据结构会用递归实现二叉树, 当然递归也不一定是最优的解决方法,就比如斐波那契数列,使用递归的话时间复杂度会很高,会出现很多冗余的运算,并没有循环来的快,这里我们就要根据实际情况来决定了。至于递归的练习题,大家可以自己去找一找,多练习练习。
本期我们只是知道方法该如何使用,还不是特别能看出来方法的重要性,等往后学习,我们就会慢慢慢把学习知识串联起来,那个时候就会明白更多,学习是循环渐进的过程,很多进步都是在写代码的途中产生的,请坚持下去。