目录
方法的定义和使用
什么是方法
在C语言中,我们能够熟练地掌握和使用函数调用;那么,在Java中的方法,其实也是类似于C语言中的函数(有或者说:Java中的方法就等于C语言中的函数)。
方法的定义
方法语法的格式:
修饰符 返回值类型 方法名称([参数类型 形参]){
方法体代码;
[return 返回值];
}
下面有几点需要注意:
1. 修饰符:目前还没有说到类和对象,所以遇到的问题直接使用public static这个固定搭配即可
2. 返回值类型:如果方法有返回值,返回值类型必须要与返回的实体类型一致,如果没有返回值,必须写成void
3. 方法名字:采用小驼峰命名
4. 参数列表:如果方法没有参数,则()中什么都不写;如果有参数,需要指定参数类型,多个参数类型之间使用逗号隔开
5. 方法体:方法内部要执行的语句
6. 在Java当中,方法必须写在类当中
7. 在Java当中,方法是不能嵌套定义的
8. 在Java当中,没有方法声明这一说
方法调用的执行过程
方法调用的过程:
调用方法 -> 传递参数 -> 找到方法地址 -> 执行被调用方法的方法体 -> 被调方法结束返回 -> 回到主调方法继续往下执行
注意:
1. 定义方法的时候,不会执行方法的代码,只有被调用的时候才会执行
2. 一个方法可以被多次调用
在Java中,方法的调用和C语言中函数的调用一样都需要函数(方法)栈针的开辟,后面会单独写一篇这样的文章来介绍函数栈针。
实参和形参的关系
与C语言一样的,在Java中,实参的值永远都是拷贝到形参中(也可以理解为形参是实参的一个临时拷贝),形参和实参本质是两个实体。
这里用一个交换两整型变量的例子来更好地了解实参和形参的关系。
public class TestDemo {
public static void main(String[] args) {
int a=10;
int b=20;
System.out.println("交换前:a="+a+" b="+b);
swap(a,b);
System.out.println("交换后:a="+a+" b="+b);
}
public static void swap(int x,int y){
int tmp=x;
x=y;
y=tmp;
}
}
很显然,上面这段代码是不能够实现两整型变量交换的,因为这段代码其实就和C语言中的按值传递一样,实参和形参是没有任何关联性的,对形参的操作是不会对实参产生任何影响的,所以在C语言中对于这样的情况是可以按址传递的,传的是实参所在的地址,这样在swap函数中解引用后对两整型变量的交换就可以实现真正的交换了。但是对于Java来说,Java本身就没有址传递这一概念(相当于没有指针之类的概念),那么,又应该怎么样实现呢?其实在Java中,是通过传引用类型参数来进行实现的(下面的代码是使用数组来解决实现的,具体的传引用操作以及数组在后面的文章中会陆续讲到,这里只需知道可以这样实现即可):
public class TestDemo {
public static void main(String[] args) {
int[] array={10,20};
System.out.println("交换前:array[0]="+array[0]+" array[1]="+array[1]);
swap(array);
System.out.println("交换后:array[0]="+array[0]+" array[1]="+array[1]);
}
public static void swap(int[] arr){
int tmp=arr[0];
arr[0]=arr[1];
arr[1]=tmp;
}
}
方法重载与方法签名
方法重载
众所周知,在C语言中同一个.c文件中是不可以有两个函数名一样的函数,因为这样在调用的时候编译器就不知道程序想要调用的究竟是哪一个函数,而与C语言不同的,在Java中,如果有多个方法的名字相同,而参数列表不同,就会构成方法重载,这样的情况下的编译器是不会报错的,其内部是会自动识别与调用相对应的那个方法。在Java中,方法的重载运用地十分广泛,频繁地被使用到,所以在Java中是非常支持程序员使用方法重载的。
使用方法重载需要注意的几点:
1. 方法名必须是相同的
2. 参数列表必须不同(只要符合这三者其一即可:参数的个数不同、参数的类型不同、类型的次序不同)
3. 与返回值类型是否相同是无关的(也就是说,如果两个方法仅仅只是返回值不同,是不能够构成重载的)
方法签名
方法签名就是经过编译器修改过之后方法的最终名字。
注:不同的方法有不同的方法签名。
public class TestDemo {
public static int add(int a,int b){
return a+b;
}
public static double add(double a,double b){
return a+b;
}
public static void main(String[] args) {
int a=10;
int b=20;
double c=10.6;
double d=22.2;
int ret1=add(a,b);
double ret2=add(c,d);
System.out.println(ret1);
System.out.println(ret2);
}
}
这段代码经过编译之后,使用JDK自带的javap反汇编工具进行查看,可以得到以下的代码:
递归
递归的概念
递归既是数学思想也是编程思想。与C语言相同的,递归都是函数(方法)调用自己本身,就比如有时候会遇到一些不太好解决的问题,但是发现将问题拆分成其子问题之后,子问题与原问题有一个相同的算法,等子问题解决之后,原问题也就随之迎刃而解了。其实,几乎所有语言都有递归这个概念,具体可以看之前的文章,当然,后面还会再出一些用Java实现递归题目的文章。
使用递归要记住一前提二要求:
一前提:推导出其一个递归公式
二要求:调用自己本身 + 有一个趋近于终止的条件
递归执行过程分析
递归的执行是纵向执行的。
“调用栈”:与C语言一样的,方法调用的时候,会有一个“栈” 这样的内存空间描述当前的调用关系。
每一次的方法调用就称为一个“栈帧”,每个栈帧中包含了这次调用的参数是哪些,返回到哪里继续执行等信息……(具体可以看之前的文章)
当然,递归的执行虽然是纵向执行的,但是在思考递归的时候,应该是横向思考的。千万不能去思考递归是如何执行的,这样会很容易绕进去,实在想看递归代码是如何执行的,可以对该段代码进行调式。
递归与循环的区别以及选择
在所有我们所写的代码中,几乎所有递归都可以用循环来替代,同样的,几乎所有循环都可以使用递归来实现。
那么,很多人在这个地方就会有疑问:应该在什么时候使用循环?在什么时候使用递归?是否有一个具体的规定或要求?
对于递归,其实并没有明确的规定,主要还是取决于自己,感觉使用递归来实现会更加符合常理,那就使用递归;相反地,就选择循环。
下面就来分别列举出递归和循环的特点:
1. 递归:代码行数比较少;逻辑比较清楚;但理解起来会比较困难;浪费的空间也会比较多;运行效率较低。
2. 循环:代码行数可能会比较多一些;理解起来会比较简单;浪费的空间比较少;运行效率比较高。
下面使用一个最经典的例子(斐波那契数列)来证明上面结论:
1. 使用递归实现斐波那契数列:
public class TestDemo {
public static void main(String[] args) {
System.out.println(fib(40));
}
public static int fib(int num){
if(num==1||num==2){
return 1;
}
return (fib(num-1)+fib(num-2));
}
}
2. 使用循环实现斐波那契数列:
public class TestDemo {
public static void main(String[] args) {
System.out.println(fib(40));
}
public static int fib(int num){
if(num==1||num==2){
return 1;
}
int n1=1;
int n2=1;
int n3=0;
for (int i = 3; i <= num; i++) {
n3=n1+n2;
n1=n2;
n2=n3;
}
return n3;
}
}
因为使用递归来实现斐波那契数列是通过不断进行分叉计算每一个数的,所以势必会有大部分是重复计算的,而且每递归一次就会在栈上开辟一块空间,而执行下一个递归方法的时候,上一个方法所开辟的栈还会保留着,并不会销毁,所以递归的同时会占用大量的空间,所以总体来说,递归的运行效率是会比较低的,这也就验证了上面的结论。再来看使用循环实现斐波那契数列,不难看出,其在得出斐波那契数列的下一位的时候,之前的计算会保留下来用在下一次计算中,避免了重复计算,再加上循环的这种思想只定义了三个变量,在占用内存空间方面并不会太大,所以总体来说,循环的执行效率大大提高,而且占用的内存空间也是比较小的,这也可以验证上面的结论。
所以,对于这种情况,往往我们是会选择使用循环来实现的;但是对于汉诺塔那些比较复杂的问题,我们如果是使用循环是非常难想周全或实现的,那么我们就会很快地考虑到使用递归来实现,因为其逻辑是比较清晰的。