目录
- 1 方法的基本用法
- 1.1 什么是方法
- 1.2 方法定义语法
- 1.3 方法调用的执行过程
- 1.4 实参和形参的关系
- 1.5 没有返回值的方法
- 2 方法的重载
- 3 方法递归
- 3.1 了解递归
- 3.2 递归练习
1 方法的基本用法
1.1 什么是方法
方法就是一个代码片段,类似于 C 语言中的 “函数”。
方法存在的意义(不用背, 重在体会):
- 是能够模块化的组织代码(当代码规模比较复杂的时候)。
- 做到代码被重复使用, 一份代码可以在多个位置使用。
- 让代码更好理解更简单。
- 直接调用现有方法开发, 不必重复造轮子。
1.2 方法定义语法
方法的基本语法结构:
// 方法定义
public static 方法返回值 方法名称([参数类型 形式参数]) {
方法体代码(也就是函数的主要实现 );
[return 返回值];
}
// 方法调用
返回值变量 = 方法名称 (实参…);
注意: 这里的返回值很重要,它是为了支持链式表达式,即可以将当前函数的返回值作为另一个参数的参数 或者 在另一个函数当中进行运算。
例如:写一个方法实现两个整数相加。
具体代码示例如下:
public class TestDemo {public static int add(int a, int b) {return a+b;}public static void main(String[] args) {int a = 10;int b = 20;System.out.println(add(a,b));}}
这里我们要清楚,任何的方法都需要在栈上开辟内存,对于栈来说,它的特性是先进后出,由高地址到低地址进行增长的。
就以上述代码为例具体描述方法所执行的过程:
- 假设我们这里有一个栈,这个函数在栈上运行时,我们遇到的第一个方法是main()方法,首先会在栈上开辟一个栈帧,这个栈帧是main()方法的栈帧。此时,在这个main()函数中,有一个a,还有一个b,它会给a和b各分配一块内存。此时在main()函数的栈帧中a的值为10,b的值为20。
- 接下来会调用add()方法,此时又会给add()方法在这个栈里开辟一个新的栈帧,在这个add()函数中,又有一个a,又有一个b,它又会给这个a和b各分配一块内存,但这里的a和b是形式参数。此时,这里的a和b所接收的值是main()函数中传过来的值,所以这个a的值也为10,b的值也为20。
- 但当方法遇到return,直接将所return的值带回,并且将当前函数(也就是add()函数)所开辟的栈帧进行销毁。当程序运行结束后,main()函数所开辟的栈帧也从栈上消失了。
这就是我们所说的局部变量的内存是在栈上的,当函数结束之后,局部变量就销毁了,上述所描述的过程就是其原因,也可借下图对其进行更清楚地理解。
并且这个函数只要你调用,你调用几次,我们就在栈上开辟几次栈帧。如下面代码所示,for()循环中调用了10次add()函数,我们就在栈上开辟了10次栈帧。具体代码示例如下:
public class TestDemo {public static int add(int a, int b) {return a+b;}public static void main(String[] args) {int a = 10;int b = 20;for (int i = 0; i < 10 ; i++) {int ret = add(a,b);System.out.println(ret);}}}
JVM对我们的内存进行了划分,JVM中内存共划分为5块,分别为:Java虚拟机栈(JVM Stack)、本地方法栈、堆、程序计数器和方法区。
我们平时所说的栈就是Java虚拟机栈,我们方法想要开辟内存,一定是在Java虚拟机栈中进行的;其中本地方法栈运行的代码是JVM底层代码;JVM(Java虚拟机)实际上就是一个软件,使用C和C++代码实现的;堆上一般存放的是对象;程序计数器存放的是指令;方法区一般存储的是静态的数据,也就是static所修饰的变量,一般都是存放在方法区的;方法区中还有一块内存是方法表,里面存放的是方法的地址等信息。
总结:
- 方法定义时,参数可以没有,但每个参数要指定类型。
- 方法定义时,返回值也可以没有,如果没有返回值,则返回值类型应写成 void。
- 方法定义时的参数称为 “形参”,方法调用时的参数称为 “实参”。
- 方法的定义必须在类之中,代码书写在调用位置的上方或者下方均可。
- Java 中没有 “函数声明” 这样的概念。
1.3 方法调用的执行过程
- 定义方法的时候, 不会执行方法的代码,只有调用的时候才会执行。
- 当方法被调用的时候, 会将实参赋值给形参。
- 参数传递完毕后, 就会执行到方法体代码。
- 当方法执行完毕之后(遇到 return 语句), 就执行完毕, 回到方法调用位置继续往下执行。
- 一个方法可以被多次调用。
1.4 实参和形参的关系
以交换两个整型变量为例子,我们大脑中所先想到的代码可能如下所示:
public class TestDemo {//此时交换的是形参的值,没有实现交换实参的值/** 写一个交换函数,交换实参的值* */public static void swap(int a, int b) {int tmp = a;a = tmp;tmp = b;}public static void main35(String[] args) {int a = 10;int b = 20;System.out.println(a+" "+b);swap(a,b);System.out.println(a+" "+b);}}
但此代码的运行结果为a = 10 b = 20。所以刚才的代码, 没有完成数据的交换。对于基础类型来说, 形参相当于实参的拷贝,即传值调用。可以看到, 对 a和 b 的修改, 不影响本身的a 和 b。
解决办法: 传引用类型参数 (例如数组来解决这个问题),具体代码示例如下:
public class TestDemo {public static void main(String[] args) {//数组是一个引用类型 引用你可以把它想象成一个低配的指针 引用实际上就是一个变量//用来存储地址//array还是一个局部变量,但它的值{10,20}在堆上面int[] array = {10,20};//定义了一个数组,Java上面的数组是在堆上面的System.out.println(array[0]+" "+array[1]);swap(array);System.out.println(array[0]+" "+array[1]);}public static void swap(int[] array2) {int tmp = array2[0];array2[0] = array2[1];array2[1] = tmp;}}
这里代码的运行结果就为a = 20 b = 10了。
原因分析: 在Java当中,栈上的地址是拿不到的,所以我们只能把变量放在堆上。假设这里有一个栈,一个堆,对int[] array = {10,20};中的array来说,它是在栈上开辟了一块内存;而{10,20}它是一个对象,这个对象在堆当中开辟了一块内存,这个内存的大小是8个字节。此时array是一个引用类型,它存放的就是当前这个对象在堆当中的地址,也就是这个引用指向了这个对象,这个指向我们认为是逻辑上的指向。并且在Java中,传值分为按值传递和引用传递。但严格来说,Java当中只有按值传递。这里可借助下图进行理解。
1.5 没有返回值的方法
方法的返回值是可选的,有些时候可以没有的,需要根据实际需求进行书写,例如上面的交换两个整数的方法, 就是没有返回值的。
2 方法的重载
有些时候我们需要用一个函数同时兼容多种参数的情况, 我们就可以使用到方法重载。例如当我们用下面这种方法写这个代码时就很麻烦,具体代码示例如下:
public class TestDemo {public static int sumInt(int a,int b){return a+b;}public static double sumDouble(double a,double b){return a+b;}public static float sumFloat(float a,float b){return a+b;}public static void main(String[] args) { System.out.println(sumInt(10,20)); System.out.println(sumDouble(3.1,1.6));}}
这个时候我们使用方法的重载就可以解决这个麻烦性,具体代码示例如下:
public class TestDemo {public static int sum(int a,int b){return a+b;}public static double sum(double a,double b){return a+b;}public static float sum(float a,float b){return a+b;}public static void main(String[] args) {System.out.println(sum(10,20));System.out.println(sum(3.1,1.6));}}
这三个方法的名字都叫sum, 但是有的 sum是计算 int 相加, 有的是 double 相加, 有的是float相加。同一个方法名字, 提供不同版本的实现, 就称为方法重载。
重载需要满足3个要求:
- 方法名相同。
- 参数列表不同(主要针对类型和个数)。
- 返回值不做要求。
注意: 当两个方法的名字相同, 参数也相同, 但是返回值不同的时候, 不构成重载。且满足重载有一个大前提就是:一定是在同一个类当中。
可变参数编程: 利用数组实现可变参数的编程。
其中int…:表示给函数sum传参数时可以传多个参数,这里只能是三个点,这是语法规定,否则就是错误的。并且传过来的数据可以看作是一个数组,但它的局限性是int… array传过来的一组数据,一定都是相同的数据类型。具体代码示例如下:
public class TestDemo {public static int sum(int... array){int ret = 0;for (int x:array) {ret += x;}return ret;}public static void main(String[] args) {System.out.println(sum(1,2,3,4,5,6));System.out.println(sum(1,2,3));System.out.println(sum(1,2));System.out.println(sum(1,2,3,4,5,6,7));}}
3 方法递归
3.1 了解递归
一个方法在执行过程中调用自身, 就称为 “递归”。递归相当于数学上的 “数学归纳法”, 有一个起始条件, 然后有一个递推公式。递归是将大问题化解为小问题的过程,说明处理大问题的方式和处理小问题的方式是一样的。要写一个成功的递归,需要推导出一个递推公式。
同时写递归时需要注意的两个条件:
- 调用自己本身。
- 有一个趋于终止的条件。
记住所谓递归,一个是递,一个是归,我们递到有一个趋近于终止的条件时就需要原路返回了。与循环相比,循环就是迭代。在这里我们以求N的阶乘举例:
- 它的起始条件为: N = 1 的时候, N! 为 1,这个起始条件相当于递归的结束条件。
- 递归公式: 求 N! , 直接不好求, 可以把问题转换成 N! = N * (N-1)!
具体代码示例如下:
public class TestDemo {public static int fac2(int n) {if(n == 1){return 1;} return n*fac2(n-1);//或者可以写成//int tmp = n*fac2(n-1);//return tmp;}public static void main(String[] args) {System.out.println(fac2(5));}}
此代码的执行过程可以借助下图进行理解:
如果你写了一个递归,在你的编译器左边就会出现这个小图标,说明你写了一个递归函数,如下图所示。
可是如果一旦你的终止条件没有或者找错了,运行程序你就会发现它出现了运行时错误,此时这个错误为StackOverflowError,叫做栈溢出了。因为一个栈一般只有1M或2M,而一个堆一般有4G,函数是在栈上运行的,而变量一般储存在堆上。当你的终止条件没有或者找错了,栈上就会不断进行函数的递归,没有递归运行的停止条件,所以就会发生把栈挤爆的现象,也叫栈溢出。具体代码示例如下:
public class TestDemo {public static int fac2(int n) {/*if(n == 1){return 1;}*/ return n*fac2(n-1);//或者可以写成//int tmp = n*fac2(n-1);//return tmp;}public static void main(String[] args) {System.out.println(fac2(5));}}
运行结果如下图所示:
我们要知道StackOverflowError的报错原因:一般就是递归的终止条件找错了或者你没写。这个叫做错误,一般程序是不能帮你解决错误的,只能由程序员自己上手解决错误。
求阶乘这个例子属于单路递归,还有多路递归,他一般用在二叉树和斐波那契数列上面。递归的代码没有技巧,只能多练。它非常的抽象,以后不要尝试展开递归的代码。递归思考是横向思考,但递归的执行是纵向执行的。
3.2 递归练习
例1:按顺序打印一个数字的每一位(例如 1234 打印出 1 2 3 4)。
问题分析:
我们发现要想按顺序得到一个数字的每一位,就可以通过不断地进行除以10操作,直到被除数小于10时,再进行取余操作即可。
例如123,它不断地进行除以10操作,直到被除数小于10(123/10 = 12 ; 12/10 = 1;),再进行取余操作(1%10 = 1),即可得到数字的第一位,再通过递归运算,我们便可得到剩余位上的数字。
具体代码示例如下所示:
public class TestDemo {public static void printf(int n) {if(n >9){printf(n/10);}System.out.println(n%10);} public static void main(String[] args) {printf(123);}}
代码运行过程可借助下图进行理解:
例2:递归求 1 + 2 + 3 + … + 10。
问题分析:
我们知道 1 + 2 + 3 + … + 10的和与10+9+8+…+1的和相等,所以你就会发现这个式子也可以写成10+sum(9)的形式,而sum(9)又可以写成9+sum(8)的形式,直到加到1停止。
具体代码示例如下所示:
public class TestDemo {public static void main(String[] args) {System.out.println(sumOR(10));}public static int sumOR(int num) {if (num == 1) {return 1;}return num + sumOR(num - 1);}}
例3:写一个递归方法,输入一个非负整数,返回组成它的数字之和。例如,输入 1729, 则应该返回1+7+2+9,它的和是19。
问题分析:
要先得到它各个位数的数字,然后在对其各个位的数字进行相加操作即可。
通过例1,我们可以得知获得各个位数的数字的方法就是不断除以10即可,例如123,它不断地进行除以10操作,直到被除数小于10(123/10 = 12 ; 12/10 = 1;),再通过取余得到这个数字,再通过递归运算将得到的数字进行相加,我们便可得到各个位的数字进行相加结果。
具体代码示例如下所示:
public class TestDemo {public static int faction(int n) {if(n > 9){int tmp =faction(n/10)+n%10;return tmp;//return n % 10 + faction(n / 10); }else{return n;}}public static void main(String[] args) {System.out.println(faction(1234));}
代码运行过程可借助下图进行理解:
例4:求斐波那契数列的第 N 项。
此题的详细解答过程见链接:https://blog.csdn.net/weixin_51312723/article/details/113540833
总结:
- 递归是一种重要的编程解决问题的方式,有些问题天然就是使用递归方式定义的(例如斐波那契数列, 二叉树等), 此时使用递归来解就很容易。
- 但有些问题使用递归和使用非递归(循环)都可以解决,那么此时更推荐使用循环, 相比于递归, 非递归程序更加高效。