1. 方法的基本用法

1.1 什么是方法

C语言中的函数,在Java中叫做”方法”
方法的作用和意义(同C):

  • 让某一功能模块化,使得这个模块能够被复用,提高开发的效率
  • 提高代码的可读性(某个方法有具体的名称)

例子:如果一道题让我们求1~100的和,我们可以直接在主函数中写出来:

  1. int sum = 0;
  2. for (int i = 1; i <= 100 ; i++) {
  3. sum += i;
  4. }
  5. System.out.println(sum);

结果:5050

但如果要求我们写1~n的和呢?每次都要重新修改,未免有些麻烦了.所以我们可以使用方法来模块化这个累加的功能

1.2 方法的定义

语法

  1. // 方法定义
  2. public static 方法返回值 方法名称([参数类型 形参 ...]){
  3. //方法体代码;
  4. [return 返回值];//视具体情况
  5. }
  6. // 方法调用 返回值变量 = 方法名称(实参...);

以累加求和为例:

  1. public class TestDemo {
  2. public static void main(String[] args) {
  3. int n = 100;
  4. int sum = Sum(n);//方法的调用
  5. System.out.println(sum);
  6. }
  7. public static int Sum(int n){
  8. //返回值类型 方法名 形参
  9. int sum = 0;
  10. for (int i = 1; i <= n ; i++) {
  11. sum += i;
  12. }
  13. return sum;//返回值
  14. }
  15. }

结果:5050

方法的使用方法和C大致相同,区别在于:

  • 方法不需要另外声明,只要将方法写在类(public class)和main函数之间即可
  • public class的特定含义后续会学习

    1.3 方法调用的过程

    规则(同C)

  • 只有方法被调用,其代码才会被执行

  • 方法被调用时,形参是实参的一份临时拷贝.参数传递后才会执行方法的具体代码,是纵向执行的.
  • 遇到return语句(不论方法是否有返回值),方法都会结束执行,回到主函数的当前位置

    1.4 实参与形参

    例:交换两个整型变量的值

    1. public static void main(String[] args) {
    2. int a = 1;
    3. int b = 2;
    4. Swap(a, b);
    5. System.out.println(a);
    6. System.out.println(b);
    7. }
    8. public static void Swap(int a, int b){
    9. int tmp = a;
    10. a = b;
    11. b = tmp;
    12. }

    结果:1 2

在学习C语言后,我们知道,这个函数并没有交换两个变量,得传地址才能交换.但Java中没有指针变量的概念,对于基础类型,只能传值调用方法,形参是实参的一份临时拷贝
如何解决:传递引用类型(区别于基础类型)参数,比如数组

  1. public static void main(String[] args) {
  2. int[] arr = {1, 2};
  3. swap(arr);
  4. System.out.println("a = " + arr[0] + " b = " + arr[1]);
  5. }
  6. public static void swap(int[] arr) {
  7. int tmp = arr[0];
  8. arr[0] = arr[1];
  9. arr[1] = tmp;
  10. }

结果:2 1

2. 方法的重载(Overload)

2.1 方法重载存在的意义

例:加法方法

  1. public static void main(String[] args) {
  2. int a = 1;
  3. int b = 2;
  4. int ret1 = Add(a, b);
  5. System.out.println(ret1);
  6. float c = 1.5f;
  7. float d = 2.5f;
  8. float ret2 = Add(c, d);
  9. System.out.println(ret2);
  10. }
  11. public static int Add(int a, int b){
  12. return a + b;
  13. }

错误:java: 不兼容的类型: 从float转换到int可能会有损失

即使将ret2的值强转为float,编译器依然显示以上错误
原因是参数类型不匹配
方法重载的意义:

解决了同一个方法需要兼容多组不同的参数的问题

如何解决?

2.1 如何使用方法重载

  1. public static void main(String[] args) {
  2. int a = 1;
  3. int b = 2;
  4. int ret1 = Add(a, b);
  5. System.out.println(ret1);
  6. float c = 1.5f;
  7. float d = 2.5f;
  8. float ret2 = Add(c, d);
  9. System.out.println(ret2);
  10. }
  11. public static int Add(int a, int b){
  12. return a + b;
  13. }
  14. public static float Add(float a, float b){
  15. return a + b;
  16. }

结果: 3 4.0

可以发现:调用的两个方法的名字是一样的,为什么编译器不会像C一样报错呢?
编译器是如何识别方法的?

  • 每一个方法都有自己的签名:

菜鸟教程中对重载的描述:

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。 注:参数类型列表即参数个数或类型

也就是说,方法的参数类型列表也是方法身份的一部分,不同于C,仅将函数名称作为函数的身份标识

2.3 方法重载的规则

在同一个类中

  • 方法名相同
  • 参数类型列表不同
  • 返回值类型不影响重载,可以改变

代码实例见2.1

附:Java 8语法规则—8.4.9. Overloading

3. 方法的递归

3.1 递归的概念

方法的递归同C中函数的递归,即函数自身调用自身.
需要注意的是,递归并不能无限地调用自身.递归在数学中的体现是”数学归纳法”
递归的条件:

  • 找到起始条件
  • *找到递推公式
  • *有一个不断趋近的(结束)条件,且存在一种简单情况,使得递归结束

“三部曲”

  1. 确定递归函数的参数和返回值
  2. 终止条件
  3. 单层递归的逻辑

例:求N!(假如N=3) 起始条件: N = 1! 结束条件 N = 3! 递推公式: N! = N (N - 1) 如:3! = 3 2! = 3 2 1! = 3 2 1

例:用方法求N!

  1. public static void main(String[] args) {
  2. int n = 5;
  3. int recursionNum = Recur(n);
  4. System.out.println(recursionNum);
  5. }
  6. public static int Recur(int n){
  7. if(n == 1)
  8. return 1;
  9. return n * Recur(n - 1);
  10. }

结果:120

3.2 递归的执行过程

代码示例见3.1例
执行过程图解:以计算3!为例
image.png
需要注意的是:并不是变调用方法,边返回值(或执行该层对应的语句).

从这里可以体会到:我们口头上说的(即形象理解的)起始条件就是程序的终止条件,另一半反之. 例如:求3! 形象的理解:3! = 1 2 3 实际上程序是先把所有能够调用的地方全部调用完毕,才会返回值 3! = 3 f(2) = 3 2 f(1) = 3 2 * 1

小结

我们理解的起始和终止条件和程序真正地起始和终止条件是相反的

3.3 练习

例1:打印数字的每一位(1234)

起始条件:1234
终止条件:最后一位小于10(0~9)
递归公式:打印最后一位+去掉最后一位

  1. public static void main(String[] args) {
  2. int n = 1234;
  3. printNum(n);
  4. }
  5. public static void printNum(int n){
  6. if( n > 9){
  7. printNum(n / 10);
  8. }
  9. System.out.print(n % 10 + " ");
  10. }

结果:1 2 3 4

此处体现了3.2提到的要点:方法调用到终止后,方法才会从最深的递归开始返回或者执行语句

例2:求1~10的和

起始条件:10
终止条件:1
递归公式:f(N) = N + f(N - 1)

  1. public static void main(String[] args) {
  2. int n = 10;
  3. int num = addNum(n);
  4. System.out.println(num);
  5. }
  6. public static int addNum(int n){
  7. if( n == 1 ){
  8. return 1;
  9. }
  10. return n + addNum(n-1);
  11. }

结果:55

例3:写一个递归方法,输入一个非负整数,返回组成它的数字之和.

如1234,结果为1+2+3+4=10
起始条件:1234
终止条件:最后一位小于10(0~9)
递归公式:取出最后一位,边取边模10

  1. public static void main(String[] args) {
  2. int n = 1234;
  3. int num = numAdd(n);
  4. System.out.println(num);
  5. }
  6. public static int numAdd(int n){
  7. if(n < 10){
  8. return n;
  9. }
  10. return n % 10 + numAdd(n / 10);
  11. }

结果:10

例4:求斐波那契数列第N项

假设N为5
起始条件:第1项为1,第2项为1
终止条件:同上
递归公式:从第三项开始,当前项等于前两项之和

  1. public static void main(String[] args) {
  2. int n = 5;
  3. int num = fib(n);
  4. System.out.println(num);
  5. }
  6. public static int fib(int n){
  7. if(n == 1 || n == 2){
  8. return 1;
  9. }
  10. return fib(n - 1) + fib(n - 2);
  11. }

结果:5

补充:斐波那契数列用递归容易理解,但效率很低.

用循环实现求斐波那契数列第N项

思路:
利用递归公式:f(N) = N + f(N - 1),(N>2)
将三个数看成一个整体,每次循环走一步,三个数同时往后走一步
(这里很像三指针的迭代)

  • 需要注意三个数往后迭代的顺序(为什么?)

计算6!:

  1. public static void main(String[] args) {
  2. int num1 = 1;
  3. int num2 = 1;
  4. int num3 = 0;
  5. for(int i = 3; i <= 6; i++){//要从第三项开始
  6. int cur = num3;
  7. num3 = num1 + num2;
  8. num1 = num2;
  9. num2 = num3;
  10. }
  11. System.out.println(num3);
  12. }

结果:8

ps:面试的时候千万不要写递归的喔(想想为什么?)

总结:

  1. Java中的方法实际上就是C语言中的函数.最大的不同点就在于方法的重载等(目前仅学习了重载),Java将形参类型列表也当做方法身份的标识
    2. 递归的要素需要在写之前就想好,磨刀不误砍柴工.起始/终止条件,递归条件
    3. 重点理解递归的调用和返回(没有返回就执行语句)是两个不同的方向.我形象的认为递归的调用方向是向下的,复杂程度取决于调用层次的深度,返回或执行语句是自下而上的,不断积累.实际上我们理解的递归是自下而上的.