目录

  • 1 方法的基本用法
    • 1.1 什么是方法
    • 1.2 方法定义语法
    • 1.3 方法调用的执行过程
    • 1.4 实参和形参的关系
    • 1.5 没有返回值的方法
  • 2 方法的重载
  • 3 方法递归
    • 3.1 了解递归
    • 3.2 递归练习

1 方法的基本用法

1.1 什么是方法

方法就是一个代码片段,类似于 C 语言中的 “函数”。
方法存在的意义(不用背, 重在体会):

  1. 是能够模块化的组织代码(当代码规模比较复杂的时候)。
  2. 做到代码被重复使用, 一份代码可以在多个位置使用。
  3. 让代码更好理解更简单。
  4. 直接调用现有方法开发, 不必重复造轮子。

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));}}

这里我们要清楚,任何的方法都需要在栈上开辟内存,对于栈来说,它的特性是先进后出,由高地址到低地址进行增长的。
就以上述代码为例具体描述方法所执行的过程:

  1. 假设我们这里有一个栈,这个函数在栈上运行时,我们遇到的第一个方法是main()方法,首先会在栈上开辟一个栈帧,这个栈帧是main()方法的栈帧。此时,在这个main()函数中,有一个a,还有一个b,它会给a和b各分配一块内存。此时在main()函数的栈帧中a的值为10,b的值为20。
  2. 接下来会调用add()方法,此时又会给add()方法在这个栈里开辟一个新的栈帧,在这个add()函数中,又有一个a,又有一个b,它又会给这个a和b各分配一块内存,但这里的a和b是形式参数。此时,这里的a和b所接收的值是main()函数中传过来的值,所以这个a的值也为10,b的值也为20。
  3. 但当方法遇到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所修饰的变量,一般都是存放在方法区的;方法区中还有一块内存是方法表,里面存放的是方法的地址等信息。
总结:

  1. 方法定义时,参数可以没有,但每个参数要指定类型。
  2. 方法定义时,返回值也可以没有,如果没有返回值,则返回值类型应写成 void。
  3. 方法定义时的参数称为 “形参”,方法调用时的参数称为 “实参”。
  4. 方法的定义必须在类之中,代码书写在调用位置的上方或者下方均可。
  5. Java 中没有 “函数声明” 这样的概念。

1.3 方法调用的执行过程

  1. 定义方法的时候, 不会执行方法的代码,只有调用的时候才会执行。
  2. 当方法被调用的时候, 会将实参赋值给形参。
  3. 参数传递完毕后, 就会执行到方法体代码。
  4. 当方法执行完毕之后(遇到 return 语句), 就执行完毕, 回到方法调用位置继续往下执行。
  5. 一个方法可以被多次调用。

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个要求:

  1. 方法名相同。
  2. 参数列表不同(主要针对类型和个数)。
  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 了解递归

一个方法在执行过程中调用自身, 就称为 “递归”。递归相当于数学上的 “数学归纳法”, 有一个起始条件, 然后有一个递推公式。递归是将大问题化解为小问题的过程,说明处理大问题的方式和处理小问题的方式是一样的。要写一个成功的递归,需要推导出一个递推公式。
同时写递归时需要注意的两个条件:

  1. 调用自己本身。
  2. 有一个趋于终止的条件。

记住所谓递归,一个是递,一个是归,我们递到有一个趋近于终止的条件时就需要原路返回了。与循环相比,循环就是迭代。在这里我们以求N的阶乘举例:

  1. 它的起始条件为: N = 1 的时候, N! 为 1,这个起始条件相当于递归的结束条件。
  2. 递归公式: 求 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
总结:

  1. 递归是一种重要的编程解决问题的方式,有些问题天然就是使用递归方式定义的(例如斐波那契数列, 二叉树等), 此时使用递归来解就很容易。
  2. 但有些问题使用递归和使用非递归(循环)都可以解决,那么此时更推荐使用循环, 相比于递归, 非递归程序更加高效。