Bootstrap

Java程序基础④Java方法和递归

目录

1. 方法的概念和意义

2. 方法的语法和执行过程

3. 形参和实参(重要)

3.1 形参和实参的概念

3.2 形参和实参的关系

3.3 数组是引用数据类型

4. 方法重载

4.1 方法重载的意义

4.2 方法重载的概念

4.3 方法重载的条件

4.4 方法重载的作用

4.5 方法签名

5. 递归

5.1 递归的概念

5.2 递归的执行过程

本篇完。


1. 方法的概念和意义

        方法是什么?在人们有目的的行动中,通过一连串有特定逻辑关系的动作或心理活动来完成特定的任务。这些有特定逻辑关系的动作所形成的集合整体就称之为人们做事的一种方法。

        在Java编程中亦是如此,将频繁使用的代码抽取出来封装成方法,需要的时候再调用方法即可,避免了代码频繁编写,提高了编程效率(在C语言有另外一个名字,它叫做函数),方法可以简单了解为一个代码片段。

方法的意义:

  1. 避免频繁编写相同的代码,导致编程效率下降。
  2. 共性的抽取,提高代码的简洁性,可读性。
  3. 直接调用现有的方法进行使用,避免重复造轮子。
  4. 是一段模块化的组织代码,作用简洁明了。

2. 方法的语法和执行过程

方法语法规则:

    public static 返回值类型 方法名(方法参数列表) {
        //方法体
        return 返回值;
    }
  • 修饰符:目前先使用“public static”修饰方法,public是访问限定修饰符,并且public是访问权限最大的,也就是说,没有访问限制。static表示静态,static修饰的方法亦或是变量,会变成静态方法。
  • 返回值:返回值类型和返回值一定要匹配,否则系统会无法编译;如果没有返回值,则返回值类型可以设置为void。
  • 在Java中,方法要定义在类中,由class修饰的都是类,一个文件中只会有一个主类,由public class修饰,剩下的类只能使用class修饰,表示副类。
  • 在Java中,没有方法说明的说法,直接就是定义方法,然后进行调用。
  • 在Java中,方法不支持嵌套定义。
  • 方法参数列表:如果没有参数()里可以什么都不写,如果有参数,并且是多个,则需要使用逗号隔开,注意使用英文逗号。
  • 方法体:方法内部需要执行的代码。

一个整数相加方法:

        public static int add(ini x , int y){
            return x + y;
        }

方法调用的执行过程:

        调用方法 -> 传递参数 -> 找到方法地址 -> 执行被调用的方法体 -> 被调用方法执行结束返回 -> 回到主调方法继续向下执行代码。

        方法在编写的时候并不会为方法分配内存,只有在调用的时候才会为方法分配内存,方法调用结束系统回收栈帧,内存回收。

方法支持多次调用,也支持递归调用。


3. 形参和实参(重要)

3.1 形参和实参的概念

        在Java中,形参相当于数学公式里面的因变量,实参相当于因变量,实参的改变会导致形参改变,并且在方法中形参的名字可以随意定义,对方法没有任何影响,形参只是在方法调用的时候需要借助的一个变量,用来接收主调方法传递过来的值。

        在Java中,实参的值永远是拷贝到实参中的,形参和实参是两个不同的实体,也可以称之为形参是实参的临时拷贝,但形参的变化不一定会影响实参。

3.2 形参和实参的关系

    // 交换两数的值
    public static void main(String[] args) {
        int x = 1;
        int y = 2;
        
        func(x , y);
        System.out.println("x = " + x + "y = " + y);
    }

    public static void func(int x , int y) {
        int tmp = x;
        x = y;
        y = tmp;
        System.out.println("x = " + x + "y = " + y);
    }

        看似在func方法中将两数的值交换了,但实际上只是将func中的x和y交换了,并不影响主调方法里面的x 和 y。也就是说方法的形参的改变不影响实参。

    实参x和y是main方法里面的两个变量,其存储在main方法的栈空间(一块特殊的存储空间)里面,而形参是存储在func方法的栈空间里面的,两个空间之间是互不影响的。因此:实参x和y与形参x和y是两个没有任何关联的变量,在func方法调用时,只是将实参x和y临时拷贝传递给形参的x和y,因此形参中对x和y的操作是不会对实参中x和y产生任何影响的,

    但有一个前提,就是实参是基本数据类型的,不是引用数据类型,如果实参是引用数据类型的,形参的变化是有可能会影响到形参的,但具体还是看形参是如何进行操作的,并不是说实参是引用数据类型的,形参就一定会改变实参的值,不可妄下结论,是有多种情况的。

    对于实参是基本数据类型的来说,形参相当于实参的临时拷贝,也就是传值调用,在 Java 中没有传址调用这个说法的,在JAVA里是没有办法直接拿到地址的。

        当func方法执行结束之后,func的栈空间被回收,相当于此时调用该方法并没有实质性的效果,但使用数组的方式就可以实现真正意义上的两数交换。

3.3 数组是引用数据类型

    // 交换两数的值
    public static void main(String[] args) {
        int x = 1;
        int y = 2;
        
        func(x , y);
        System.out.println("x = " + x + "y = " + y);
    }

    public static void func(int x , int y) {
        int tmp = x;
        x = y;
        y = tmp;
        System.out.println("x = " + x + "y = " + y);
    }

 没有返回值的方法:

        方法的返回值并不是说一定要有,当你需要使用方法的返回值时,就可以设置返回值,不需要返回值也可以设置返回值类型为void,表示无返回值。

    public static void func(int[] array) {
        int tmp = array[0];
        array[0] = array[1];
        array[1] = tmp;
    }

4. 方法重载

4.1 方法重载的意义

        在编写代码的时候,当编写方法的时候,方法的作用类似但又不相同,在C语言中只能老老实实的重新创建一个函数,有时候就会因为给函数起名字而烦恼,但JAVA不一样,JAVA支持方法的重载,就避免了创建作用类似的多个方法,也就不必因为起名字而烦恼。

    public static int add1(int a , int b) {
        return a + b;
    }
    public static int add2(int a , int b , int c) {
        return a + b + c;
    }
    public static int add3(int a , int b ,int c , int d) {
        return a + b + c + d;
    }

        当你需要实现两数相加的方法,三数相加的方法,四数相加的方法时,如果是如上这般创建的话,简单粗暴的解决,看起来确实很不错,但实际上并不美观,并且需要提供许多的方法名,像add1,add2确实好起名字,但是当你需要调用数量不同的数相加时你还需要上来看一下代码,具体多少个数字相加是那个方法,一来二去,就会让你感到繁琐,这个时候,重载的效果就出来了。

4.2 方法重载的概念

        在自然语音中,一个词语有多个含义,那么就说该词语被重载了,具体代表什么含义需要集合场景进行使用,在JAVA中的方法也是可以重载的。

    在Java中,如果多个方法的名字相同,参数列表不同,则称之为这几个方法构成了重载。通俗的理解就是:省了起名字的过程。

    public static int add1(int a , int b) {
        return a + b;
    }
    public static int add2(int a , int b , int c) {
        return a + b + c;
    }
    public static int add3(int a , int b ,int c , int d) {
        return a + b + c + d;
    }

4.3 方法重载的条件

  1. 方法名字相同,方法体可以不同也可以相同。
  2. 方法的参数列表不同:参数类型不同,参数个数不同,参数顺序不同(如果类型是相同的,顺序不同不构成重载,必须是要求类型不同的前提这下才构成重载)。
  3. 与返回值类型是否相同无关。
    public static int add(int a , int b , int c) {
        return a + b + c;
    }
    public static int add(int b , int a , int c) {
        return a + b + c;
    }

上面的代码两个方法的参数列表顺序不同,但因为类型相同的原因并不构成重载。


    public static int add(int a , int b , double c) {
        return a + b + c;
    }
    public static int add(int b , int a , double c) {
        return a + b + c;
    }

上面的代码参数类型不同,构成重载。


        方法的返回值不做要求(也就是返回值相同和不相同都不影响构成重载,返回值的不同不是构成重载的标准)。

    public static int add(int a , int b) {
        return a + b;
    }

    public static void add(int a , int b , int c) {
        System.out.println(a + b + c);
    }

    public static int add(int a , int b ,int c , int d) {
        return a + b + c + d;
    }

三者都构成重载。


4.4 方法重载的作用

        方法重载的主要好处就是不用为了对不同的参数类型或参数个数,而写多个方法。多个方法用同一个名字,但参数表,即参数的个数或数据类型可以不同,调用的时候,虽然方法名字相同,但根据参数表可以自动调用对应的函数。这样我们在调用的时候,就不需要记那么多的方法名称,而是知道了方法的功能就可以直接的给他传递不同的参数,编译器会明确的知道我们调用了哪一个方法。重载比if…else要优雅,减少了if…else部分的代码。

        重载的最直接作用是方便了程序员可以根据不同的参数个数,顺序,类型,自动匹配方法,减少写多个方法名的重复步骤。


4.5 方法签名

        在同一个作用域中不能定义两个名称相同的标识符。比如:同一个类中不能定义名字相同的变量;在同一个包中,不能够定义两个名字相同的类(包中有类,类中有方法,方法中有变量),那为什么可以定义相同的方法名呢?这个时候就要引出方法签名的存在了:

        方法签名:经过编译器修改后方法最终的名字。也就是说我们在编译的时候写的方法名并不是真正的方法名,而是编译器想要我们看到的方法名。

方法名具体方式:方法全路径名 + 参数列表 + 返回值类型,构成完整的方法名。

package rtx;
import java.util.Scanner;

public class Hello {
    public static void main(String[] args) {
    }

    public static int add(int a , int b) {
        return a + b;
    }
    public static void add(int a , int b , int c) {
        System.out.println(a + b + c);
    }

    public static int add(int a , int b ,int c , int d) {
        return a + b + c + d;
    }
}

当我们将上述代码进行编译了之后,来到字节码文件的目录之下(一般在out目录):

在此目录打开cmd后,在cmd操作里面输入javap -c 类名

        虽然是实际的方法名不同而导致的方法名相同也可以存在同一个类中,但是构成重载的标准并不是因为方法名不同,也就是说方法签名不是评判构成重载的条件,而是为什么系统名字的方法可以存在同一个类里面的原因,二者没有直接关系。


5. 递归

5.1 递归的概念

        递归,在方法的实现中调用自己。也就是自身中又包含了自己,该思想者数学和编程中非常有用,因为有些时候,我们遇到问题并不好直接解决,但是发现将原问题拆分成其子类问题之后,子问题和原问题有相同的解法,等子问题解决了之后,原问题就好解决了。

    所谓递归是指:若在一个方法,过程或者数据结构定义的内部又直接或间接的出现定义本身的应用,则称其是递归的,或者是递归定义的。

        一个方法在执行过程中调用自身, 就称为 "递归"。递归相当于数学上的 "数学归纳法", 有一个起始条件, 然后有一个递推公式。

        例如, 我们求 N! 起始条件: N = 1 的时候,N! 为 1。 这个起始条件相当于递归的结束条件,递归公式: 求 N!,直接不好求,可以把问题转换成 N! => N * (N-1)!

递归的必要条件:

  1. 将原问题划分成其子问题,注意:子问题必须要与原问题的解法相同。
  2. 有递归出口。

5.2 递归的执行过程

代码示例:递归求 N 的阶乘:

package rtx;
import java.util.Scanner;

public class Hello {
    public static void main(String[] args) {
        int n = 5;
        int res = func(n);
        System.out.println(res);
    }

    public static int func(int n) {
        if(n == 1) { // 结束条件
            return 1;
        }
        return n * func(n - 1); // 递归公式
    }
}

        想要理解递推的过程就需要先了解递归这个词语,什么是递归?是不是需要先递推然后再回归呢?那么递归就好理解了好多,顾名思义就是:先递推到结束条件,再由结束条件一步一步递推回来。

        在我们的方法中,需要执行完一个方法结束了之后才可以继续调用下一个方法,在递归中,当我们调用第一个方法时,当方法1执行到方法体里面的自调用方法时,此时进入并执行该方法2,当方法2执行到自调用方法时,又进入并执行方法3,以此类推,一直执行到不会在方法体里面不会调用该方法时,开始return回归,此时由一步一步的回归到上一个方法并且执行return。

        上图中黑线先执行,一步一步的执行到n==1的时候,return返回1,此时递推结束,开始回归,也就是将每次递推的经过回归(红线执行)。

        当然递归并不是说说什么问题都可以解决,因为递归是单路递归,只能一条路走到黑,也就是方法体里面只能有一个自调用方法,但有些比较特殊的需要用到多路递归,可以简单理解为方法体里面有二个以上的自调用方法体,虽然递归也可以解决,但因为递归本质上是单路递归的原因,用递归解决多路递归的问题会导致代码的执行效率下降,并且会出现同一个代码多次调用点情况,也就是代码冗余,简而言之就是问题应该用最合适的方法解决,而不是用不适合的方法强行解决,这样会导致意想不到的结果发生。比如斐波那契数列就不适合用递归解决。

        当n比较小的时候运行速度还是很快的,但随着n的增大,代码计算的冗余开始增多,代码的执行效率将开始下降了。

package rtx;
import java.util.Scanner;

public class Hello {
    public static void main(String[] args) {
        int n = 10;
        int ret = func1(n);
        System.out.println("递归计算次数" + count1);

        int num = func2(n);
        System.out.println("非递归计算次数" + count2);
    }

    public static int count1 = 0;
    public static int func1(int n){
        if(n == 1 || n == 2) {
            count1++;
            return 1;
        }
        return func1(n - 2) + func1 (n - 1);
    }

    public static int count2 = 0;
    public static int func2(int n) {
        if(n == 1 || n== 2) {
            count2++;
            return 1;
        }
        int n1 = 1;
        int n2 = 1;
        int num = 0;
        for(int i = 3 ; i <= n ; i++) {
            count2++;
            num = n1 + n2;
            n2= n1;
            n1 = num;
        }
        return num;
    }
}

        所谓非递归方法,其实就是迭代,但实际上迭代其实就是循环,通过count计数器的次数我们可以清晰的看到用递归的方法解决斐波那契数列需要的计算次数要比循环要多,那间接说明了,递归并不适合求解斐波那契数列,所以什么方法代码效率高就用那个。


本篇完。

下篇是Java程序基础⑤Java数组的定义和使用。

;