Skip to content

方法、参数、重载与递归 ​

学习目标 ​

  • 会定义和调用有返回值、无返回值、静态和实例方法,并设计清楚参数与职责。
  • 理解 Java 的按值传递,区分基本值副本与引用值副本对对象状态的影响。
  • 会写常见方法重载和可变参数,知道 null、重载歧义和输入校验的边界。
  • 能为递归方法写出基线条件,并判断什么时候应该改用循环。

核心知识点 ​

专业术语 ​

  • 方法(method):带名称、参数列表和返回类型的可调用代码单元。
  • 形参/实参(parameter/argument):方法声明中的接收变量与调用时传入的实际值。
  • 返回值(return value):方法通过 return 交给调用者的结果;void 方法不返回值。
  • 按值传递(pass-by-value):方法接收实参值的副本;引用类型传递的是引用值的副本。
  • 重载(overload):同一作用域内方法名相同但参数列表不同,选择发生在编译期。
  • 可变参数(varargs):T... 在调用处可以接收零个或多个参数,方法内表现为 T[]。
  • 递归(recursion):方法直接或间接调用自己,必须有基线条件和规模缩小步骤。

白话解释与边界 ​

方法定义与调用 ​

方法由访问修饰符、返回类型、名称、参数列表和方法体组成;调用时写出方法名和实参,返回值可以赋给变量或继续参与表达式。

返回值与 void ​

方法签名包含名称、参数类型和顺序,返回类型不能单独构成重载;void 方法只表达动作,不向调用方返回值。

参数与按值传递 ​

Java 没有“按引用传递”:基本类型参数复制数值,对象参数复制引用值。方法可以通过复制的引用修改同一个可变对象,但把形参重新指向新对象不会改变调用方变量。若不希望方法改动对象,应传入不可变对象、复制数据或明确约定方法的副作用。

重载与编译期选择 ​

重载让同一个概念接受不同参数形式;编译器在编译期根据静态参数类型选择最匹配的方法,运行时不会根据返回类型重新选择。多个可变参数或 null 候选可能产生歧义,因此重载应保持语义相关、差异明显。

可变参数 varargs ​

可变参数必须是最后一个参数,调用时可以传零个值,也可以传一个数组。它只是数组语法糖,显式传入 null 时得到的是 null 数组而不是空数组,方法需要先定义是否接受。为了减少歧义,不要在同一作用域设计多个只靠可变参数区分的重载。

递归与基线条件 ​

递归每次都要靠近基线条件,否则会无限递归并最终 StackOverflowError。递归适合树、分治和天然嵌套的数据;简单计数和大规模线性处理通常用循环,避免调用栈随输入增长。输入校验应在方法入口完成,让核心计算只处理合法状态。

职责拆分与输入校验 ​

一个方法最好只完成可命名的一件事:入口校验输入,核心方法计算结果,展示层负责输出。这样调用方更容易复用,也能分别验证边界和正常路径。

设计点常用做法边界
返回值用结果表达计算,void 表达只产生副作用所有分支都要满足返回类型要求
参数传入完成职责所需的最少信息过多参数应拆分对象或职责
重载同名方法表达同一概念的不同参数形式不能只改返回类型,避免 null 调用歧义
varargs末尾参数接收零到多个值方法内是数组,需处理空数组与 null
递归明确基线与规模缩小深度不可控时优先循环或显式栈

常用用法 ​

方法重载:提供不同参数入口 ​

重载只能通过参数列表区分,不能只改返回类型;多个重载遇到 null 时还要留意调用歧义。

java
// 语义:重载只能通过参数列表区分,不能只改返回类型。
public class OverloadDemo {
    static int add(int left, int right) {
        return left + right;
    }

    static int add(int value) {
        return add(value, 0);
    }

    public static void main(String[] args) {
        System.out.println(add(2));
        // 输出:2
    }
}

可变参数:接收零到多个值 ​

可变参数在方法内部就是数组,必须放在参数列表最后,并明确空数组与显式 null 的行为。

java
// 语义:可变参数在方法内部就是数组,必须放在参数列表最后,并明确空数组与显式 null 的行为。
// 初始状态:total 初始为 0。
public class VarargsDemo {
    static int sum(int... values) {
        int total = 0;
// 关键变化:total 接收表达式 0 的计算结果。
// 初始状态:total 的初始值为 0。
        for (int value : values) total += value;
        return total;
    }

    public static void main(String[] args) {
        System.out.println(sum(1, 2, 3));
        // 输出:6
    }
}

不常用但需要知道 ​

递归:用基线条件缩小问题 ​

每次递归都必须靠近基线条件;深度受输入影响时优先考虑循环或显式栈。

java
public class RecursionDemo {
    static int factorial(int value) {
        return value <= 1 ? 1 : value * factorial(value - 1);
    }

    public static void main(String[] args) {
        System.out.println(factorial(4));
        // 输出:24
    }
}

方法引用:复用已有方法 ​

方法引用只是 Lambda 的简写,参数和返回类型仍必须与目标函数式接口匹配。

java
import java.util.function.Function;

public class MethodReferenceDemo {
    public static void main(String[] args) {
        Function<String, Integer> length = String::length;
// 关键变化:length 接收表达式 String::length 的计算结果。
// 初始状态:length 的初始值为 String::length。
        System.out.println(length.apply("Java"));
        // 输出:4
    }
}

简单案例 ​

java
import java.util.Arrays;

public class MethodDemo {
    static int add(int left, int right) {
        return left + right;
    }

    static String format(int value) {
        return "number=" + value;
    }

    static String format(String value) {
        return "text=" + value;
    }

    static int sum(int... values) {
        if (values == null) {
            throw new IllegalArgumentException("values must not be null");
        }
        int total = 0;
        for (int value : values) {
            total += value;
        }
        return total;
    }

    static int factorial(int n) {
        if (n < 0) {
            throw new IllegalArgumentException("n must be non-negative");
        }
        if (n <= 1) {
            return 1;                 // 基线条件
        }
        return n * factorial(n - 1);  // 问题规模减 1
    }

    static void rename(String[] names) {
        if (names.length > 0) {
            names[0] = "Ada";         // 修改同一个数组对象
        }
    }

    static void reassign(String[] names) {
        names = new String[] {"New"}; // 只改变形参副本
    }

    public static void main(String[] args) {
        String[] names = {"Old"};
        rename(names);
// 关键变化:rename(names) 把共享数组第 0 项从 "Old" 改为 "Ada"。
        reassign(names);
// 关键变化:reassign(names) 只让形参指向新数组,调用方的 names 仍为 [Ada]。
        System.out.println(Arrays.toString(names));
        // 输出:[Ada]
        System.out.println(add(2, 3) + ", " + format(7) + ", " + format("ok"));
        // 输出:5, number=7, text=ok
        System.out.println("empty=" + sum() + ", values=" + sum(1, 2, 3));
        // 输出:empty=0, values=6
        System.out.println("factorial=" + factorial(4));
        // 输出:factorial=24
    }
}

rename 通过复制的引用值修改调用方看到的数组内容,reassign 只让形参指向新数组,所以调用方仍看到原数组。sum() 的零参数调用得到空数组,显式传入 null 则会按入口校验抛出异常;factorial 用 n <= 1 作为基线,递归参数每次减小。

易混点 ​

  • Java 参数始终按值传递;对象参数传的是引用值副本,不是让方法获得调用方变量本身。
  • 方法重载看参数列表,不看返回类型;format(1) 与 format("1") 可区分,不能只靠返回类型区分。
  • 可变参数是数组,sum() 是空数组,sum((int[]) null) 是 null 数组,二者边界不同。
  • 递归必须同时有基线条件和规模缩小;只有“调用自己”而没有这两点就是无限递归。
  • 方法职责应聚焦一个可命名动作;输入校验、计算和输出混在一起会让复用与测试都变困难。

课后小问 ​

  1. reassign(names) 为什么不能让调用方的 names 指向新数组,而 rename(names) 却能看到元素变化? 答案:两个方法都收到引用值副本,但 rename 修改的是副本引用所指向的同一个数组对象,reassign 只是让形参副本改指向新对象。 解析:调用方变量保存的引用值没有被方法直接替换;要返回新对象,应让方法返回数组并由调用方接收,或使用明确的可变容器协议。

  2. 为什么不能用返回类型区分两个同名方法? 答案:调用表达式可能只关心参数和方法名,编译器无法仅凭返回值决定调用哪个版本,因此 Java 不允许只改返回类型的重载。 解析:int parse(String) 和 long parse(String) 不能同时声明;应改用不同方法名,或让一个明确的返回类型覆盖需求。

  3. 递归方法为什么必须有基线条件? 答案:基线条件让某个输入直接返回,其他输入逐步缩小规模,最终一定能停下来。 解析:没有基线或参数不变时,调用栈会不断增长并抛出 StackOverflowError;即使有基线,也应估算深度,超大输入优先使用循环。

本节小结 ​

  • 方法通过参数接收数据并用返回值表达结果,职责清晰比堆叠大方法更容易复用。
  • Java 始终按值传递,引用值副本可以修改共享对象,但不能替换调用方变量的引用。
  • 重载由参数列表决定,可变参数本质是数组,空数组与 null 数组必须区分。
  • 递归需要基线和规模缩小;深度不可控时应改用循环或显式栈结构。

快速回顾 ​

  • 能写出有返回值、void、静态和实例方法的基本声明与调用方式。
  • 能用数组对象案例解释 Java 的按值传递,而不是“按引用传递”。
  • 能判断重载是否只改变了返回类型,以及 varargs 是否位于最后一个参数。
  • 能为递归方法指出基线条件、规模变化和潜在栈深度风险。

继续阅读 ​