方法、参数、重载与递归
学习目标
- 会定义和调用有返回值、无返回值、静态和实例方法,并设计清楚参数与职责。
- 理解 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 时还要留意调用歧义。
// 语义:重载只能通过参数列表区分,不能只改返回类型。
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 的行为。
// 语义:可变参数在方法内部就是数组,必须放在参数列表最后,并明确空数组与显式 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
}
}不常用但需要知道
递归:用基线条件缩小问题
每次递归都必须靠近基线条件;深度受输入影响时优先考虑循环或显式栈。
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 的简写,参数和返回类型仍必须与目标函数式接口匹配。
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
}
}简单案例
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数组,二者边界不同。 - 递归必须同时有基线条件和规模缩小;只有“调用自己”而没有这两点就是无限递归。
- 方法职责应聚焦一个可命名动作;输入校验、计算和输出混在一起会让复用与测试都变困难。
课后小问
reassign(names)为什么不能让调用方的names指向新数组,而rename(names)却能看到元素变化? 答案:两个方法都收到引用值副本,但rename修改的是副本引用所指向的同一个数组对象,reassign只是让形参副本改指向新对象。 解析:调用方变量保存的引用值没有被方法直接替换;要返回新对象,应让方法返回数组并由调用方接收,或使用明确的可变容器协议。为什么不能用返回类型区分两个同名方法? 答案:调用表达式可能只关心参数和方法名,编译器无法仅凭返回值决定调用哪个版本,因此 Java 不允许只改返回类型的重载。 解析:
int parse(String)和long parse(String)不能同时声明;应改用不同方法名,或让一个明确的返回类型覆盖需求。递归方法为什么必须有基线条件? 答案:基线条件让某个输入直接返回,其他输入逐步缩小规模,最终一定能停下来。 解析:没有基线或参数不变时,调用栈会不断增长并抛出
StackOverflowError;即使有基线,也应估算深度,超大输入优先使用循环。
本节小结
- 方法通过参数接收数据并用返回值表达结果,职责清晰比堆叠大方法更容易复用。
- Java 始终按值传递,引用值副本可以修改共享对象,但不能替换调用方变量的引用。
- 重载由参数列表决定,可变参数本质是数组,空数组与
null数组必须区分。 - 递归需要基线和规模缩小;深度不可控时应改用循环或显式栈结构。
快速回顾
- 能写出有返回值、
void、静态和实例方法的基本声明与调用方式。 - 能用数组对象案例解释 Java 的按值传递,而不是“按引用传递”。
- 能判断重载是否只改变了返回类型,以及
varargs是否位于最后一个参数。 - 能为递归方法指出基线条件、规模变化和潜在栈深度风险。
阿源的知识库