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);:

  1. 把 10 和 20 分别传给 x、y。
  2. 进入 add,输出“正在计算”。此时 main 等待这次调用完成。
  3. 执行 return x + y;,得到 30,结束本次 add 调用。
  4. 回到原调用位置,相当于完成 int result = 30;。
  5. 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 递归的两个关键条件

一个方法在执行过程中调用自身,就是递归。 它适合把原问题转化成“规模更小、解法相同”的子问题。

写递归前,先回答:

  1. 什么时候直接得到答案,不再调用自己? 这是递归出口。
  2. 怎样把问题缩小,并利用子问题的结果? 每次调用都要向出口靠近。

只有出口但永远到不了出口,也不能正确结束。

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

递归适合表达问题的拆分关系,但不代表一定更高效。判断时既要看能否到达出口,也要看是否反复计算相同的子问题。