1. 方法是什么,怎样定义和调用
方法就是把完成某个功能的代码放在一起,起一个名字,需要时通过这个名字调用。 同一段逻辑可以反复使用,也能把复杂任务拆成几个更容易理解的小任务。
理解一个方法,先看三件事:接收什么数据(参数)、完成什么操作(方法体)、交回什么结果(返回值)。 方法也可以没有参数,或者没有返回值。
1.1 从一个完整程序入手
public class MethodDemo { public static void main(String[] args) { System.out.println("调用前"); int result = add(10, 20); System.out.println("调用后,结果为:" + result); } public static int add(int x, int y) { System.out.println("正在计算"); return x + y; } } 输出:
调用前 正在计算 调用后,结果为:30 add 的定义可以这样读:
| 部分 | 含义 |
|---|---|
public static | 修饰符,本章先按这个固定写法使用,具体含义后续学习 |
int | 返回值类型,表示方法交回一个 int 结果 |
add | 方法名,通常使用小驼峰命名,如 isLeapYear |
int x, int y | 参数列表,表示调用时需要提供两个整数 |
{ ... } | 方法体,调用时要执行的代码 |
return x + y; | 把计算结果交回调用处,并结束本次方法调用 |
一般格式为:
public static 返回值类型 方法名(参数类型 形参名, 参数类型 形参名, ...) { 方法体 } - 没有参数时,括号写成
();有多个参数时,用逗号分隔,每个参数都要写类型。 - 没有返回值时,返回值类型写
void。 - 本章的方法写在类的
{}内,与main并列,不能把方法直接定义在另一个方法内部。 - 同一个类中,方法写在
main前面或后面都可以,不需要在调用前另写一条声明。
1.2 一次调用到底怎样执行
对上面的 int result = add(10, 20);:
- 把
10和20分别传给x、y。 - 进入
add,输出“正在计算”。此时main等待这次调用完成。 - 执行
return x + y;,得到30,结束本次add调用。 - 回到原调用位置,相当于完成
int result = 30;。 -
main继续执行下一条语句。
定义方法不会自动执行方法体,调用才会执行;方法结束后,会回到调用处继续执行。 同一个方法可以被多次调用,每次使用这次传入的数据重新执行。
2. 返回值:把结果交回去,不是打印出来
2.1 返回、接收、打印是三件事
假设 add 只负责计算,不包含打印语句:
public static int add(int x, int y) { return x + y; } 下面的语句放在同一个类的 main 中:
add(1, 2); // 执行计算,但丢弃返回值,不会输出 int result = add(1, 2); // 用变量保存返回值 System.out.println(result); // 输出 3 System.out.println(add(3, 4)); // 直接输出返回值 7 int twice = add(1, 2) * 2; // 返回值参与运算,twice 为 6 有返回值不代表一定要用变量接收,但不使用它,就得不到这次计算的后续用途。 是否输出,取决于有没有执行打印语句。
例如,求 1 + 2 + ... + n 后再乘以 2:
public static int getSum(int n) { return (1 + n) * n / 2; } 在 main 中调用 int result = getSum(10) * 2;,先得到 55,再计算出 110。
2.2 返回值类型必须兼容
方法返回的数据,要能赋给声明的返回值类型;接收结果的变量,也要能保存这个结果。
int a = add(1, 2); // 正确 double b = add(1, 2); // 正确:int 可以自动转换为 double 因此,“类型匹配”不等于必须逐字相同。反过来,不能把一个 double 返回值直接赋给 int 变量。
返回值也可以是布尔值。例如判断闰年:
public static boolean isLeapYear(int year) { return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; } 条件表达式本身就是 true 或 false,可以直接返回。调用 System.out.println(isLeapYear(2024)); 会输出 true。
执行到 return 就会结束本次方法调用,不会继续向后执行。 对本章这些有返回值的方法,要保证每条可能正常结束的分支都能返回结果,不能只在某个 if 分支中返回,其他情况却没有返回值。
2.3 不需要交回结果时,用 void
public static void print(int x, int y) { System.out.println("x = " + x + ",y = " + y); } 在 main 中:
print(10, 20); // 正确:直接调用 // int result = print(10, 20); // 错误:void 方法没有值可以赋给变量 void 不表示“返回 0”,而是不返回任何值。这种方法可以执行到末尾结束,也可以使用不带值的 return; 提前结束。
选择返回值时,问自己:调用者是否还要使用计算结果?需要就返回;如果任务只是打印内容,不需要交回结果,就可以用 void。
3. 参数传递:形参拿到的是实参值的副本
3.1 分清形参和实参
public static int add(int x, int y) { // x、y 是形参 return x + y; } 调用 add(a, 20) 时,a 和 20 是实参。
- 形参:定义方法时写的变量,用来接收传入的数据。
- 实参:调用方法时实际提供的值,可以是常量、变量或表达式。
- 调用时按位置传递:第一个实参给第一个形参,第二个给第二个,依此类推。
- 实参与形参的个数、顺序和类型要匹配;类型允许合法的自动转换。
例如,只有 getSum(int n) 时,getSum(10) 合法,而 getSum(10.9) 会编译失败,因为 double 不能直接传给 int 形参。
形参不要求与调用处的变量同名。add(a, b) 中的 a、b 与方法里的 x、y 是不同变量。
3.2 为什么 swap(a, b) 交换不了 a、b
public class SwapDemo { public static void main(String[] args) { int a = 10; int b = 20; swap(a, b); System.out.println("a = " + a + ",b = " + b); } public static void swap(int x, int y) { int temp = x; x = y; y = temp; System.out.println("x = " + x + ",y = " + y); } } 输出:
x = 20,y = 10 a = 10,b = 20 执行过程:
| 阶段 | main 中的变量 | swap 中的变量 |
|---|---|---|
| 调用时复制值 | a = 10,b = 20 | x = 10,y = 20 |
| 交换形参后 | a = 10,b = 20 | x = 20,y = 10 |
| 调用结束后 | a = 10,b = 20 | 本次调用中的局部变量不再使用 |
Java 始终按值传递参数。 对这里的 int 来说,复制的是整数值,x、y 不是 a、b 的别名,所以交换的是副本。
本章先掌握基本类型参数的这个结论;借助类和对象处理类似需求的方法,留到相应章节。
4. 用方法拆分任务:计算阶乘之和
计算 1! + 2! + 3! + 4! + 5!,可以拆成两个职责:
-
factorial(n):只负责求一个数的阶乘。 -
main:依次调用,再把结果相加。
public class FactorialSumDemo { public static int factorial(int n) { int result = 1; for (int i = 1; i <= n; i++) { result *= i; } return result; } public static void main(String[] args) { int sum = 0; for (int i = 1; i <= 5; i++) { sum += factorial(i); } System.out.println(sum); // 153 } } 五次调用依次返回 1、2、6、24、120,相加得到 153。每次进入 factorial,都会重新创建并初始化自己的 result,不会沿用上一次的值。
这样写是把内部循环封装进方法,使任务更清楚;并没有消除内部循环,也没有自动减少运算次数。
5. 方法重载:同一个名字,接收不同参数
5.1 为什么需要重载
两个整数相加、两个小数相加、三个小数相加,做的都是“相加”,没必要分别起三个名字。可以使用同一个方法名,通过参数列表区分。
public class OverloadDemo { public static int add(int x, int y) { return x + y; } public static double add(double x, double y) { return x + y; } public static double add(double x, double y, double z) { return x + y + z; } public static void main(String[] args) { System.out.println(add(1, 2)); // 3 System.out.println(add(1.5, 2.5)); // 4.0 System.out.println(add(1.5, 2.5, 3.5)); // 7.5 } } 本章在同一个类中判断重载:方法名相同,参数列表不同。 编译器根据调用处的参数个数和类型等信息,确定调用哪个方法。
5.2 怎样才算“参数列表不同”
下面只比较方法名和参数类型,省略方法体:
| 变化 | 示例 | 能否构成重载 |
|---|---|---|
| 参数个数不同 | add(int, int) 与 add(int, int, int) | 能 |
| 参数类型不同 | add(int, int) 与 add(double, double) | 能 |
| 参数类型的排列顺序不同 | add(int, double) 与 add(double, int) | 能 |
| 只改变形参名字 | add(int x, int y) 与 add(int a, int b) | 不能 |
| 只改变返回值类型 | int add(int, int) 与 double add(int, int) | 不能 |
前三种差异满足一种即可,并不是要求同时满足。把两个 int 形参换个位置,类型序列仍是 (int, int),也不能构成重载。
返回值类型不参与重载的区分。 例如 double result = add(1, 2);,在上例中调用的仍是 add(int, int),然后把返回的 int 转换成 double,不会因为左边是 double 就选择小数版本。
5.3 方法签名与字节码中的描述符
对于本章的普通方法,方法签名可以理解为“方法名 + 参数类型列表”,不包括形参名字和返回值类型。例如 add(int, int) 与 add(double, double) 的签名不同,所以可以重载。参见 Java 语言规范:方法签名。
编译后,.class 字节码中还会使用方法描述符记录参数类型和返回值类型。它与源代码层面的方法签名不是同一概念,也不是把方法名改成一个长名字。
可使用 JDK 自带的 javap 查看。先编译,再进入 .class 所在目录执行:
javac OverloadDemo.java javap -v OverloadDemo.class 描述符的格式是 (参数类型)返回值类型。例如:
| 方法 | 描述符 | 读法 |
|---|---|---|
int add(int x, int y) | (II)I | 接收两个 int,返回 int |
double add(double x, double y) | (DD)D | 接收两个 double,返回 double |
void main(String[] args) | ([Ljava/lang/String;)V | 接收 String 数组,没有返回值 |
描述符中的符号:
| 符号 | 含义 |
|---|---|
V | void,用于返回值位置 |
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
[ | 数组,一个 [ 表示一维,如 [I 表示 int[] |
L类的内部名; | 类类型,如 Ljava/lang/String; 表示 String |
数组可以先理解为存放一组同类型数据的结构;这里的类名使用 / 分隔包名,记住 String 对应的写法即可,具体用法后续学习。
描述符包含返回值类型,并不意味着 Java 源代码允许仅凭返回值类型重载。 这两层规则要分开理解。参见 Java 虚拟机规范:方法描述符。
6. 递归:把问题交给规模更小的自己
6.1 递归的两个关键条件
一个方法在执行过程中调用自身,就是递归。 它适合把原问题转化成“规模更小、解法相同”的子问题。
写递归前,先回答:
- 什么时候直接得到答案,不再调用自己? 这是递归出口。
- 怎样把问题缩小,并利用子问题的结果? 每次调用都要向出口靠近。
只有出口但永远到不了出口,也不能正确结束。
6.2 递归求阶乘
对正整数 n:
-
1! = 1,作为出口。 -
n! = n × (n - 1)!,把求n!转化为求(n - 1)!。
public static int factor(int n) { if (n == 1) { return 1; } return n * factor(n - 1); } 调用 factor(5) 得到 120。这个版本的输入要求是 n >= 1,不能传入 0 或负数,否则 n - 1 会不断远离出口;示例还要求结果不超出 int 范围。
6.3 重点:递进去之后,还要逐层返回
以 factor(4) 为例:
向下调用: factor(4) 等待 4 * factor(3) factor(3) 等待 3 * factor(2) factor(2) 等待 2 * factor(1) factor(1) 返回 1,停止继续调用 向上返回: factor(2) 得到 1,计算 2 * 1,返回 2 factor(3) 得到 2,计算 3 * 2,返回 6 factor(4) 得到 6,计算 4 * 6,返回 24 return n * factor(n - 1); 不是遇到 return 就立刻跳走,而是先算出它后面的表达式,再把结果返回。因此,必须等待 factor(n - 1) 完成,才能乘以本层的 n。
每一层调用都有自己的 n。进入 factor(3) 不会把上一层的 n = 4 改成 3。
可以加入输出,观察进入和返回的顺序:
public static int factorTrace(int n) { System.out.println("进入:" + n); if (n == 1) { System.out.println("返回:1"); return 1; } int result = n * factorTrace(n - 1); System.out.println("返回:" + result); return result; } 调用 factorTrace(3) 的输出:
进入:3 进入:2 进入:1 返回:1 返回:2 返回:6 6.4 调用栈为什么能记住每一层
每次方法调用都会建立一个栈帧,保存这次调用的参数、局部变量、执行位置等信息;这些尚未结束的调用组成调用栈。
可以把它想成一叠记录纸:调用新方法时放上一张,方法返回时取走最上面一张,继续处理下面那张。因而,最后进入的方法先返回。
在最深处,调用关系是 main → factor(4) → factor(3) → factor(2) → factor(1)。factor(1) 返回后,恢复的是 factor(2),不会直接跳过中间几层回到 main。在 IDEA 调试时,可以通过调用栈查看这些层次。
7. 递归练习:从拆分规律写出代码
以下方法都写在类中,在 main 中调用。示例使用非负整数或正整数,具体范围见各题;计算结果需在 int 能表示的范围内。
7.1 按顺序打印一个非负整数的每一位
对 1234,num / 10 得到前面的 123,num % 10 得到最后一位 4。要从左到右打印,就先处理前面的部分,再打印当前最后一位。
public static void printDigits(int num) { if (num > 9) { printDigits(num / 10); } System.out.println(num % 10); } 调用 printDigits(1234),会分四行输出 1、2、3、4。
printDigits(1234):先处理 123,等它结束再打印 4 printDigits(123) :先处理 12,等它结束再打印 3 printDigits(12) :先处理 1,等它结束再打印 2 printDigits(1) :不再递归,直接打印 1 返回时依次打印 2、3、4。递归调用之前的代码在向下调用时执行,之后的代码在返回后执行。 如果把打印放到递归调用前,就会先打印末位,顺序变为 4、3、2、1。
这里没有显式写 return 作为出口:当 num <= 9 时,跳过递归调用,打印后方法自然结束。输入 0 也会正确打印 0。
7.2 求 1 + 2 + ... + n
输入要求 n >= 1。设结果为 sumTo(n):
- 出口:
sumTo(1) = 1。 - 拆分:
sumTo(n) = n + sumTo(n - 1)。
public static int sumTo(int n) { if (n == 1) { return 1; } return n + sumTo(n - 1); } sumTo(10) 返回 55。它和递归求阶乘的结构相同,只是把乘法换成了加法。
7.3 求一个非负整数的各位数字之和
对 1729,把问题拆成“最后一位 9 + 前面 172 的各位数字之和”。剩下一位数时,直接返回这个数。
public static int digitSum(int num) { if (num < 10) { return num; } return num % 10 + digitSum(num / 10); } digitSum(1729) = 9 + digitSum(172) = 9 + 2 + digitSum(17) = 9 + 2 + 7 + digitSum(1) = 9 + 2 + 7 + 1 = 19 注意两类题的区别:求 1 到 n 的和,每次用 n - 1;处理一个整数的各位数字,每次用 num / 10 去掉最后一位。
7.4 求斐波那契数列第 n 项
本题从第 1 项开始计数:1,1,2,3,5,8,13,...。前两项为 1,从第三项起,每项等于前两项之和,输入要求 n >= 1。
public static int fib(int n) { if (n == 1 || n == 2) { return 1; } return fib(n - 1) + fib(n - 2); } 例如 fib(6) 返回 8。
这个写法简洁,但会重复计算相同的子问题。
fib(5) ├── fib(4) │ ├── fib(3) │ └── fib(2) └── fib(3) ← 同一个 fib(3) 又算了一次 输入越大,重复计算越严重。计算一次 fib(40) 会得到 102334155,其中 fib(3) 会被调用 39088169 次。
若要统计次数,可以在类中、方法外定义 public static int count = 0;,并在 fib 的出口判断之后、递归调用之前加入:
if (n == 3) { count++; } count 是静态成员变量,这里只需理解为各次调用共同使用的计数器;每次重新统计前将其设为 0,调用结束后打印它。不能在 fib 内定义一个普通局部变量 count = 0 代替,因为每层调用都会有自己独立的计数。
本题可以用循环避免重复计算,只保留前两项:
public static int fibLoop(int n) { if (n == 1 || n == 2) { return 1; } int last2 = 1; // 当前要计算的项的前两项 int last1 = 1; // 当前要计算的项的前一项 int current = 0; for (int i = 3; i <= n; i++) { current = last2 + last1; last2 = last1; last1 = current; } return current; } 每次先算出新的一项,再把“前两项、前一项”向后移动:
| 本次计算 | 更新前 last2 | 更新前 last1 | current |
|---|---|---|---|
| 第 3 项 | 1 | 1 | 2 |
| 第 4 项 | 1 | 2 | 3 |
| 第 5 项 | 2 | 3 | 5 |
| 第 6 项 | 3 | 5 | 8 |
递归适合表达问题的拆分关系,但不代表一定更高效。判断时既要看能否到达出口,也要看是否反复计算相同的子问题。
