Java 笔记 12:Java 方法的相关使用,方法重载、参数传递,以及递归等内容

一、前言

记录时间 [2024-05-02]

系列文章简摘:
Java 笔记 01:Java 概述,MarkDown 常用语法整理
Java 笔记 02:Java 开发环境的搭建,IDEA / Notepad++ / JDK 安装及环境配置,编写第一个 Java 程序
Java 笔记 09:Java 流程控制相关,常见的三种控制结构(顺序、选择、循环)
Java 笔记 11:Java 方法相关内容,方法的设计原则,以及方法的定义和调用

更多 Java 相关文章,请参考上面专栏哦。

本文讲述 Java 方法的相关使用,包含方法重载、参数传递,以及递归等内容。同时,文章仔细分析了值传递和引用传递的区别,并使用阶乘案例分析递归与栈机制之间的联系


二、方法重载

方法重载是指在同一个类中定义多个方法,它们具有相同的名称但参数列表不同的特性。

Java 允许方法重载,这样可以提高代码的灵活性和可读性,使得同一个方法名可以根据参数的不同而执行不同的操作。


1. 方法重载规则

方法重载的规则主要涉及方法名称和参数列表。方法名称相同时,编译器会根据调用方法的参数个数、参数类型等去逐个匹配,以选择对应的方法,如果匹配失败,则编译器报错。

以下是方法重载的规则:

  • 方法名称相同: 重载的方法必须具有相同的名称。
  • 参数列表不同
    • 参数列表必须不同,这包括参数的数量、类型或者顺序
    • 如果参数列表中的参数数量不同,或者参数类型不同,或者参数类型相同但顺序不同,都会产生方法重载。
  • 返回类型可以相同也可以不同
    • 方法重载的返回类型可以相同也可以不同,只要参数列表不同即可。
  • 参数类型的提升
    • 如果没有完全匹配的方法,Java 会尝试通过提升参数类型来匹配。
    • 参数类型的提升是指将参数转换为更大的数据类型,比如从 intdouble
  • 变长参数(可变参数)
    • 可变参数的方法与重载方法之间存在一些模糊的关系。例如,一个方法接受一个 int[] 和一个接受多个 int 参数的方法可能会导致方法重载冲突。
    • 但是,如果没有更好的匹配,Java 会优先选择非可变参数的方法。

2. 重载示例

以下是一个方法重载的示例:

在这个示例中,Calculator 类定义了三个名为 add 的方法,它们的参数列表分别为两个整数、三个整数和两个双精度浮点数。

Main 类中调用了这些重载的 add 方法,并根据传递的参数类型和数量执行了不同的方法。

public class Calculator {
    // 重载的方法 1:两个整数相加
    public int add(int a, int b) {
        return a + b;
    }
    
    // 重载的方法 2:三个整数相加
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    // 重载的方法 3:两个双精度浮点数相加
    public double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        
        // 调用重载的方法 1
        System.out.println("Sum of 5 and 3: " + calc.add(5, 3));
        
        // 调用重载的方法 2
        System.out.println("Sum of 5, 3, and 2: " + calc.add(5, 3, 2));
        
        // 调用重载的方法 3
        System.out.println("Sum of 2.5 and 3.5: " + calc.add(2.5, 3.5));
    }
}

三、参数传递

1. 命令行传参

命令行传参是指在运行 Java 程序时,可以从命令行向程序传递参数。这些参数可以在程序运行时被访问和使用。

在 Java 中,命令行参数通过 main 方法的参数来接收,main 方法的参数是一个 String 数组,其中存储了命令行传递的参数。

下面是一个简单的示例,演示了如何从命令行传递参数给 Java 程序:

public class CommandLineArguments {
    public static void main(String[] args) {
        // 打印传递给程序的参数数量
        System.out.println("Number of command line arguments: " + args.length);
        
        // 遍历并打印每个参数
        for (int i = 0; i < args.length; i++) {
            System.out.println("Argument " + i + ": " + args[i]);
        }
    }
}

假设将上述代码保存在名为 CommandLineArguments.java 的文件中。

在命令行 CMD 中,通过以下命令编译该文件:

javac CommandLineArguments.java

编译成功后,可以使用以下命令来运行程序并传递参数:

# 如果 CommandLineArguments.java 使用了包机制
# 那么在命令行中,需要回退到 package 的最外层执行
# 例如 java com.test.method.CommandLineArguments  arg1 arg2 arg3
java CommandLineArguments arg1 arg2 arg3

在这个示例中,arg1arg2arg3 是命令行传递给程序的参数。程序将打印出传递的参数数量以及每个参数的值。


2. 可变参数

可变参数是 Java 中一种特殊的参数类型,允许方法接受可变数量的参数。在方法声明中,可变参数使用三个点 ... 来表示。

  • 它必须是方法参数列表中的最后一个参数
  • 在方法内部,可变参数被视为一个数组,允许以数组的形式访问。

以下是一个示例说明可变参数的使用:

public class VarArgsExample {
    
    // 方法使用可变参数来计算一组整数的总和
    public static int sum(int... numbers) {
        int total = 0;
        for (int num : numbers) {
            total += num;
        }
        return total;
    }

    public static void main(String[] args) {
        // 调用 sum 方法,传递不同数量的参数
        System.out.println("Sum of 1, 2, 3, 4, 5: " + sum(1, 2, 3, 4, 5));
        System.out.println("Sum of 10, 20, 30: " + sum(10, 20, 30));
        System.out.println("Sum of 2, 4, 6, 8, 10, 12: " + sum(2, 4, 6, 8, 10, 12));
    }
}

在这个示例中,sum 方法使用可变参数来计算一组整数的总和。

main 方法中调用了 sum 方法,传递了不同数量的参数。

sum 方法内部,参数 numbers 被视为一个数组,可以像操作普通数组一样对其进行处理。


3. 值传递

值传递是一种参数传递的方式,它是指将参数的值复制一份传递给方法或函数,而不是传递参数本身。在值传递中,被调用方法的形参接收的是参数值的副本,对形参的修改不会影响到原始参数。

在 Java 中,所有的基本数据类型(如 intdoubleboolean 等)都是通过值传递传递的。这意味着将一个基本数据类型作为参数传递给方法时,方法接收到的是参数值的副本,而不是原始参数本身。

以下是一个简单的 Java 示例说明值传递:

public class ValuePassingExample {
    public static void main(String[] args) {
        int x = 10;
        // 调用方法前 x 的值为 10
        System.out.println("Before calling modifyValue method, x = " + x);
        
        // 调用方法,传递参数值的副本
        modifyValue(x);
        
        // 调用方法后,x 的值仍为 10
        // 这说明,对形参的修改不会影响到原始参数
        System.out.println("After calling modifyValue method, x = " + x);
    }
    
    public static void modifyValue(int n) {
        // 修改的是形参 n 的值
        n = 20;
        
        // 形参的值被修改为 20
        System.out.println("Inside modifyValue method, n = " + n);
    }
}

在这个示例中,modifyValue 方法接收一个整数参数 n,并将其值修改为 20。然而,这个修改只影响到了方法内部的形参 n,并没有影响到原始参数 x

因此,main 方法中输出的 x 的值仍然是10,说明参数的值并没有被修改。


4. 引用传递

引用传递是一种参数传递的方式,它是指将参数的引用(内存地址)传递给方法或函数,而不是参数值的副本。在引用传递中,被调用方法的形参接收到的是原始参数的引用,这意味着对形参的修改会影响到原始参数

在 Java 中,所有的对象引用都是通过引用传递传递的。这意味着将一个对象作为参数传递给方法时,方法接收到的是对象的引用,而不是对象本身(详见后面易混淆点)。因此,对对象的任何修改都会影响到原始对象

以下是一个简单的 Java 示例说明引用传递:

public class ReferencePassingExample {
    public static void main(String[] args) {
        StringBuilder str = new StringBuilder("Hello");
        
        // 调用方法前,str 实例对象的值为 Hello
        System.out.println("Before use method, str = " + str);
        
        // 调用方法,传递对象的引用
        modifyStringBuilder(str);
        
        // 调用方法前,str 实例对象的值为 Hello World
        // 说明,对对象的任何修改都会影响到原始对象
        System.out.println("After use method, str = " + str);
    }
    
    public static void modifyStringBuilder(StringBuilder s) {
        
        // 修改对象的内容,在字符串后面追加
        s.append(" World"); 
        System.out.println("Inside modify method, s = " + s);
    }
}

在这个示例中,modifyStringBuilder 方法接收一个 StringBuilder 对象的引用,并将其内容追加了 " World"。这个修改影响到了原始对象 str,因此在 main 方法中输出的 str 的值是 “Hello World”。这说明对对象的修改是被传递的,因为参数是对象的引用,而不是对象本身的副本。


5. 易混淆点

首先明确一点:Java 中的所有参数传递都是按值传递的,包括对象引用。

关于对象引用:

对象引用传递给方法的是对象的内存地址,而不是对象本身。

这是因为在 Java 中,对象引用本质上是指向内存中对象的地址,而不是对象本身。

重点:地址!!!

通俗得说,就是比如,有一个 int 类型的变量 a,那这个 a 同时拥有地址和内容

  • a 的地址:在内存中的位置
  • a 的内容:100

那么通过类比,对象的地址,就好比它在内存中的位置。

将一个对象引用作为参数传递给方法时,实际上是将对象的地址传递给了方法。

这意味着方法中的形参和原始对象引用都指向了相同的对象


当创建一个对象时,实际上是在内存中分配了一块空间,并返回一个指向这块空间的引用。如果将这个引用同时赋值给另一个变量,那么这两个变量就指向了同一个对象,即它们都引用了相同的内存地址。因此,当通过一个引用修改对象时,另一个引用也会得到这个修改。

就好比有两个数组 str1 和 str2,str1 == str2 并不是它们的内容相同,而是指向的地址相同。换句话说就是,它们其实指向同一个数组,只要修改其中一个,另一个也会被修改。

因此,在方法内部通过形参修改对象时,实际上是通过相同的引用修改了原始对象。

结论:

尽管 Java 中的参数传递是按值传递的,但当传递的值是对象引用时,对于对象的修改会影响到原始对象。


四、递归

1. 使用递归

递归是指一个方法在内部调用自身的过程,也就是方法自己调用自己。

利用递归可以用简单的程序来解决一些复杂的问题。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

递归的能力在于用有限的语句来定义对象的无限集合。

在编程中,递归是一种解决问题的方法,其中问题的解决方案依赖于解决相同问题的小规模实例的解决方案。

在 Java 中,递归函数包括两个主要部分:

  • 基本情况(递归头):基本情况是递归算法终止的条件。如果满足了基本情况,递归函数将不再调用自身,而是返回一个确定的值。
  • 递归情况(递归体):递归情况是指递归函数调用自身的情况。在递归情况中,问题被分解为更小的相似问题,然后通过递归调用解决这些子问题。

2. 递归实现阶乘

阶乘概念

阶乘是一个数学概念,阶乘在组合数学和概率统计中有广泛的应用,例如排列和组合的计算,以及概率问题中的排列组合计数。

阶乘表示从 1 到给定整数之间所有整数的乘积。通常用符号 ! 表示。

例如,阶乘 5,写作 5!,表示从 1 到 5 之间所有整数的乘积,即:
5 ! = 5 × 4 × 3 × 2 × 1 = 120 5!=5×4×3×2×1=120 5!=5×4×3×2×1=120
阶乘的定义包括以下几点:

  1. 基本情况:0 的阶乘定义为 1。因为乘法中的乘数是一个数和它之前的所有数的乘积,如果没有任何数,则乘积为 1。
  2. 递归情况:对于正整数 n,n 的阶乘表示 n 乘以 (n-1) 的阶乘。也就是说,

n ! = n × ( n − 1 ) ! n!=n×(n-1)! n!=n×(n1)!


阶乘实现

以下是一个经典的递归示例,计算阶乘的函数:

在编程中,计算阶乘也是一个常见的任务,特别是在递归算法中。

public class Factorial {
    
    // 计算阶乘的递归函数
    public static int factorial(int n) {
        
        // 基本情况:当 n 等于 0 或 1 时,返回 1
        if (n == 0 || n == 1) {
            return 1;
        }
        
        // 递归情况:返回 n 乘以 (n-1) 的阶乘
        // 就是在进行阶乘操作 n(n-1)(n-2)...1
        return n * factorial(n - 1);
    }

    public static void main(String[] args) {
        int n = 5;
        System.out.println("Factorial of " + n + ": " + factorial(n));
    }
}

在这个示例中,factorial 方法是一个递归函数,它计算一个整数的阶乘。

在方法的基本情况中,当参数 n 等于 0 或 1 时,返回 1;在递归情况中,返回 n 乘以 factorial(n - 1) 的结果。

通过递归调用,这个方法会将问题不断分解,直到满足基本情况为止。


原理分析

如图所示,当使用递归方式计算阶乘时,每次递归调用都会在内存中创建一个新的栈帧,用于存储方法的局部变量、参数和返回地址。这些栈帧被按照先进后出的顺序排列在栈中。

  • 首先,在 main 方法调用 factorial(5) 时,会创建一个栈帧用于存储参数 n 的值为 5,然后调用 factorial(4)
  • factorial(4) 中,又会创建一个栈帧用于存储参数 n 的值为 4,然后调用 factorial(3),以此类推,直到递归到达基本情况。
  • 一旦递归到达基本情况(n=1),开始从栈顶逐步弹出栈帧,并计算阶乘的结果。
  • 每个弹出的栈帧都包含了一个部分阶乘的结果,并且返回到上一个调用点,直到最终的阶乘结果返回到 main 方法中。

在这里插入图片描述


3. 递归与栈机制

递归和栈机制之间有着密切的关系,因为递归函数的调用过程实际上就是通过栈来管理的。

递归调用过程

  • 当一个方法被递归调用时,当前方法的执行状态(包括局部变量、参数等)被保存在栈中的帧 frame 中。
  • 每次递归调用都会创建一个新的帧,它包含了该次调用的参数和局部变量。这些帧按照先进后出 FILO 的顺序存储在栈中。
  • 当递归到达基本情况时,也就是递归的终止条件,开始从栈中逐步弹出帧,依次执行每个帧中的代码,直到回到最初的调用点

栈溢出

  • 递归调用会导致栈的不断增长,如果递归深度过大,超出了栈的容量,就会发生栈溢出 Stack Overflow 错误。
  • 为了避免栈溢出,可以通过优化递归算法,尽量减少递归深度,或者使用迭代替代递归。

案例分析

下面是一个简单的示例,说明递归调用过程中栈的使用:

public class StackExample {
    public static void main(String[] args) {
        recursiveMethod(3);
    }
    
    public static void recursiveMethod(int n) {
        if (n <= 0) {
            System.out.println("Reached the base case");
        } else {
            System.out.println("Entering recursiveMethod(" + (n - 1) + ")");
            recursiveMethod(n - 1);
            System.out.println("Exiting recursiveMethod(" + (n - 1) + ")");
        }
    }
}

在这个示例中,recursiveMethod 方法被递归调用三次,每次递归调用都会创建一个新的帧,保存了当前方法的执行状态。

当递归到达基本情况时,开始从栈中逐步弹出帧,依次执行每个帧中的代码。


五、总结

本文讲述 Java 方法的相关使用,包含方法重载、参数传递,以及递归等内容。同时,文章仔细分析了值传递和引用传递的区别,并使用阶乘案例分析递归与栈机制之间的联系


一些参考资料

狂神说 Java 零基础:https://www.bilibili.com/video/BV12J41137hu/
TIOBE 编程语言走势: https://www.tiobe.com/tiobe-index/
Typora 官网:https://www.typoraio.cn/
Oracle 官网:https://www.oracle.com/
Notepad++ 下载地址:https://notepad-plus.en.softonic.com/
IDEA 官网:https://www.jetbrains.com.cn/idea/
Java 开发手册:https://developer.aliyun.com/ebook/394
Java 8 帮助文档:https://docs.oracle.com/javase/8/docs/api/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值