云计算百科
云计算领域专业知识百科平台

JavaSE:方法的使用及递归

一、方法概念及使用

方法就是一个代码片段. 类似于 C 语言中的 "函数"。

方法的定义

语法格式

// 方法定义
修饰符 返回值类型 方法名称([参数类型 形参 …]){
方法体代码;
[return 返回值];
}

例如:写一个方法实现判断是否为闰年

public class Test{
public static void main(String[] args){
int year = 2008;
boolean ret = isLeapYear(year);
if(ret == true){
System.out.println("是闰年!");
}else{
System.out.println("不是闰年!");
}
}
public static boolean isLeapYear(int year){
if(year % 4 == 0 && year %100 != 0 || year % 400 == 0){
return true;
}
return false;
}
}

【注意事项】

  • 修饰符:现阶段直接使用public static 固定搭配
  • 返回值类型:如果方法有返回值,返回值类型必须要与返回的实体类型一致,如果没有返回值,必须写成 void
  • 方法名字:采用小驼峰命名
  • 参数列表:如果方法没有参数,()中什么都不写,如果有参数,需指定参数类型,多个参数之间使用逗号隔开
  • 方法体:方法内部要执行的语句
  • 在java当中,方法必须写在类当中
  • 在java当中,方法不能嵌套定义
  • 在java当中,没有方法声明一说
  • 方法调用的执行过程

    调用方法—>传递参数—>找到方法地址—>执行被调方法的方法体—>被调方法结束返回—>回到主调方法继续往下执行

    【注意事项】

    • 定义方法的时候, 不会执行方法的代码. 只有调用的时候才会执行.
    • 一个方法可以被多次调用.

    实参与形参的关系

    在Java 中的形参和实参与 C语言的类似

    我们在调用方法的时候,传递的参数就是实参,它是一个确定的值;而被调用的方法的括号内就是形参形参的名字可以随意取,对方法都没有任何影响,形参只是方法在定义时需要借助的一个变量,用来保存方法在调用时传递过来的实参,方法调用结束后,形参就会被回收

    例如:

    public static void main(String[] args){

    getSum(10); // 10是实参,在方法调用时,形参N用来保存10
    getSum(100);// 100是实参,在方法调用时,形参N用来保存100
    }
    public static int getSum(int N){   // N是形参
       return (1+N)*N / 2;
    }
    //又比如:
    add(2, 3);   // 2和3是实参,在调用时传给形参a和b

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

    注意:

    在Java中,实参的值永远都是拷贝到形参中,形参和实参本质是两个实体

    例如:实现两个整型变量的交换

    public class TestMethod {
    public static void main(String[] args) {
    int a = 10;
    int b = 20;
    swap(a, b);
    System.out.println("main: a = " + a + " b = " + b);
    }

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

    但是,在swap函数交换之后,形参x和y的值发生了改变,但是main方法中a和b还是交换之前的值,即没有交换成功

    // 运行结果

    swap: x = 20 y = 10

    main: a = 10 b = 20

    【原因分析】

    实参a和b是main方法中的两个变量,其空间在main方法的栈(一块特殊的内存空间)中,而形参x和y是swap方法中 的两个变量,x和y的空间在swap方法运行时的栈中

    因此:实参a和b 与 形参x和y是两个没有任何关联性的变量, 在swap方法调用时,只是将实参a和b中的值拷贝了一份传递给了形参x和y,因此对形参x和y操作不会对实参a和b 产生任何影响

    即对于基础类型来说, 形参的改变不会影响实参,形参相当于实参的拷贝. 即 传值调用

    那么按照C语言的解决方式:就是 传址调用 ,在给函数传参的时候,传地址&,然后用指针变量来接收(通过&取地址操作来改变实参的值)

    但是,在Java当中是拿不到栈上的局部变量的地址的,也没有指针的概念

    【解决办法】: 传引用类型参数 (包括字符串和数组,例如数组来解决这个问题)

    (此代码等学习完数组就能看懂)

    public class TestMethod {
    public static void main(String[] args) {
    int[] arr = {10, 20};
    swap(arr);
    System.out.println("arr[0] = " + arr[0] + " arr[1] = " + arr[1]);

    }

    public static void swap(int[] arr) {
    int tmp = arr[0];
    arr[0] = arr[1];
    arr[1] = tmp;
    }
    }

    // 运行结果

    arr[0] = 20 arr[1] = 10

    没有返回值的方法

    方法的返回值是可选的. 有些时候可以没有的,没有时返回值 类型必须写成void

    示例:

    public class Test {
    public static void main(String[] args) {
    int a = 10; int b = 20; print(a, b);
    }

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

    另外,上面的交换两个整数的方法,就是没有 返 回 值 的

    二、方法重载

    为什么需要方法重载

    当我们想要写一个方法实现两个数的相加:

    public static int add(int a,int b){
    return a+b;
    }
    public static void main5(String[] args) {
    int x = 1;
    int y = 2;
    int ret = add(x,y);

    double d1 = 1.0;
    double d2 = 2.0;
    double ret1 = add(d1,d2);
    System.out.println(ret1);
    }

    运行起来时,发现报错了:

    // 编译出错

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

    double ret2 = add (a2,b2) ;

                 ^

    由于参数类型不匹配,所以不能直接使用现有的 add 方法

    一般解决方式都是每个类型创建一个方法并取一个专门的方法名:

    public static void main5(String[] args) {
    int x = 1;
    int y = 2;
    int ret = addInt(x,y);
    System.out.println(ret);

    double d1 = 1.0;
    double d2 = 2.0;
    double ret1 = addDouble(d1,d2);
    System.out.println(ret1);
    }
    public static int addInt(int a,int b){
    return a+b;
    }
    public static double addDouble(double a,double b){
    return a+b;
    }

    上述代码确实可以解决问题,但不友好的地方是:需要提供许多不同的方法名,而取名字本来就是让人头疼的事情。那能否将所有的名字都给成 add 呢?

    方法重载概念

    在自然语言中,经常会出现“一词多义”的现象,比如,“方便”,可以表示内急,表示方便面等

    一个词语如果有多重含义,那么就说该词语被重载了,具体代表什么含义需要结合具体的场景。 在Java中方法也是可以重载的。

    在Java中,如果多个方法的名字相同参数列表不同,则称该几种方法被重载了

    public class Test{
    public static void main(String[] args){
    int a = 10;
    int b = 20;
    int ret = add(a,b); //调用add(int, int)
    System.out.println(ret);

    double d1 = 1.0;
    double d2 = 2.0;
    double ret1 = add(d1,d2);//调用add(double, double)
    System.out.println(ret1);
    }

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

    当我们写完add方法不同类型时,每次我们想要调用add方法,就会浮现add的方法重载:

    注意:重载的条件

  • 方法名必须相同
  •  参数列表必须不同(参数个数不同 / 参数类型不同 / 类型顺序不同–>必须是不同类型的顺序不同)
  • 与返回值类型是否相同无关
  • 编译器在编译代码时,会对实参类型进行推演,根据推演的结果来确定调用哪个方法
  • 注意:两个方法如果仅仅只是因为返回值类型不同,是不能构成重载的

    注意:参数列表的类型顺序不同必须是不同类型的参数顺序不同,否则会报错

    方法签名

    在同一个作用域中不能定义两个相同名称的标识符。比如:方法中不能定义两个名字一样的变量,那为什么类中就可以定义方法名相同的方法呢?(即方法重载实现方式)—— 通过方法签名实现

    方法签名即:经过编译器编译修改过之后方法最终的名字。

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

    方法签名的关键要素:

  • 方法名
  • 参数列表(参数类型、顺序、数量/个数)
  • 例如以下的代码:

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

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

       public static void main(String[] args) {
           add(1,2);
           add(1.5, 2.5);
      }
    }

    上述代码经过编译之后,然后使用JDK自带的javap反汇编工具查看,具体操作:

  • 先对工程进行编译生成.class字节码文件
  • 在控制台中进入到要查看的.class所在的目录
  • 输入:javap -v 字节码文件名字即可
  • 以整型类型为例:

    总结

    方法重载的实现是编译器通过方法签名生成唯一的方法标识符

    add(int a, int b) → 编译为 add(II)I
    II = 两个int参数
    I = 返回int类型
    add(double a, double b) → 编译为 add(DD)D
    add(int a, int b, int c) → 编译为 add(III)I

    方法签名中的一些特殊符号说明:

    特殊字符 数据类型
    V void
    Z boolean
    B byte
    C char
    S short
    I int
    J long
    F float
    D double
    [ 数组(以 [ 开头,配合其他的特殊字符,表述对应数据类型的数组,几个 [ 表述几维数组)
    L 引用类型,以L开头,以 ; 结尾,中间是引用类型的全类名

    三、递归

    一个方法在执行过程中调用自身, 就称为 "递归".

    递归相当于数学上的 "数学归纳法", 有一个起始条件, 然后有一个递推公式.

    例如, 我们求 N!

    起始条件: N = 1 的时候, N! 为 1. 这个起始条件相当于递归的结束条件.

    递归公式: 求 N! , 直接不好求, 可以把问题转换成 N! => N * (N-1)!

    递归的必要条件:

  • 将原问题划分成其子问题,注意:子问题必须要与原问题的解法相同
  • 递归出口
  • 递归执行过程分析

    递归的程序的执行过程不太容易理解, 要想理解清楚递归, 必须先理解清楚 "方法的执行过程", 尤其是 "方法执行结束 之后, 回到调用位置继续往下执行".

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

    public static void main(String[] args){
    int n = 5;
    int ret = factor(n);
    System.out.println("ret=" + ret);
    }

    public static int factor(int n){
    System.out.println("函数开始, n = " + n);
    if(n == 1){
    System.out.println("函数结束, n = 1 ret = 1");
    return 1;
    }
    int ret = n * factor(n-1); // factor 调用函数自身
    System.out.println("函数结束, n = " + n + " ret = " + ret);
    return ret;
    }

    // 执行结果

    函数开始, n = 5

    函数开始, n = 4

    函数开始, n = 3

    函数开始, n = 2

    函数开始, n = 1

    函数结束, n = 1 ret = 1

    函数结束, n = 2 ret = 2

    函数结束, n = 3 ret = 6

    函数结束, n = 4 ret = 24

    函数结束, n = 5 ret = 120

    ret = 120

    执行过程图

    程序按照序号中标识的 (1) -> (8) 的顺序执行

    关于 "调用栈"

    方法调用的时候, 会有一个 "栈" 这样的内存空间描述当前的调用关系. 称为调用栈. 每一次的方法调用就称为一个 "栈帧", 每个栈帧中包含了这次调用的参数是哪些, 返回到哪里继续执行等信息.

    递归的练习

    按顺序打印一个数字的每一位

    例如 1234 打印出 1 2 3 4

    思路:使用 /10 ,%10 的方法

    结束条件(即起始条件):n < 10(当n小于10的时候,任何数字 /10 都是它本身,直接打印即可)

    递推公式:print(n / 10)  ,   n %10  

    public static void main(String[] args){
    System.out.println(print(1234));
    }

    public static void print(int n){
    if(n < 10){
    System.out.println(n);
    return;
    }
    print(n / 10);
    System.out.println(n % 10);
    }

    执行过程:

    递归求 1 + 2 + 3 + … + 10

    思路:求1+到10 ——> 就是 1+2+3+……+n  的问题,即 我们可以利用公式 n + (n-1) 来求解

    结束条件:n == 0(n == 1)

    递推公式:n + sum(n-1)

    public static void main(String[] args){
    System.out.println(sum(10));
    }

    public static int sum(int n){
    if(n == 0){
    return 0;
    }
    return n + sum(n-1);//55
    }

    执行过程:

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

    例如,输入 1729, 则应该返回 1+7+2+9,它的和是19

    思路:/10  ,%10

    结束条件:n < 10

    递推公式:fun(n /10) + n % 10

    public static void main(String[] args){
    System.out.println(fun(1729));
    }

    public static int fun(int n){
    if(n < 10){
    return n;
    }
    return fun(n/10)+n%10;
    }

    执行过程:

    求斐波那契数列的第 N 项

    斐波那契数列,又称黄金分割数列

    斐波那契数列指的是这样一个数列:

    这个数列从第3项开始,每一项都等于前两项之和

    斐波那契数列被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n – 1)+F(n – 2)

    递推求斐波那契数列的第N项

    结束条件:n < 3

    递推公式:fib(n – 1) + fib(n – 2)

    public static void main(String[] args){

    System.out.println(fib(1));//1
    System.out.println(fib(5));//5
    System.out.println(fib(10));//55
    System.out.println(fib(40));//102334155
    }

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

    执行过程:

    递归过程拆解(从顶层向下计算):

  • 计算 F (5),需先求 F (4) 和 F (3)
  • 计算 F (4),需先求 F (3) 和 F (2)
  • F (2) 是基础条件,直接返回 1
  • 计算 F (3)(F (4) 的左分支),需先求 F (2) 和 F (1)
  • F (2)=1,F (1)=1,因此 F (3)=1+1=2
  • 此时 F (4) = F (3) + F (2) = 2 + 1 = 3
  • 计算 F (3)(F (5) 的右分支),同理得 F (3)=2(与步骤 3 结果相同)
  • 最后 F (5) = F (4) + F (3) = 3 + 2 = 5
  • (当计算完左分支后,再计算右分支)

    最终结果:斐波那契数列第五项为 5

    我们看图可以知道,其实使用递归求斐波那契数列效率并不高,因为它在递归的过程有许多重复的地方(重复计算了F(3),F(2),F(1)),使用循环求解更高效

    循环求斐波那契数列的第N项

    思路:在 fib 方法内创建三个变量f1,f2,f3,表示第n-1项,第n项,第n+1项,给它们初始化都为1

    使用for循环遍历,初始值设为i=3,即如果n满足此条件即可进入循环,进行前两项相加等于第n项的值f3,求完后将原本的f2赋值给f1,f3赋值给f2,以便进行下一次循环,最后返回f3;如果不满足循环条件,则直接返回f3初始化的值(即n是第1/2项)

    public static void main(String[] args){

    System.out.println(fib(1));//1
    System.out.println(fib(5));//5
    System.out.println(fib(10));//55
    System.out.println(fib(40));//102334155
    }

    public static int fib(int n){
    int f1 = 1;
    int f2 = 1;
    int f3 = 1;
    for(int i = 3;i <= n; i++){
    f3 = f1 + f2;
    f1 = f2;
    f2 = f3;
    }
    return f3;
    }

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » JavaSE:方法的使用及递归
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!