上一篇我们用循环解决了“在同一个地方重复执行”。但循环只能在它所在的位置一遍遍转圈。真正的麻烦是:同一段逻辑要在程序的好几个地方用,甚至要在循环里一次次被调用。把代码复制三遍能跑,可一旦要改,就得同时改三处,改漏一处就出 bug。

方法(method)就是冲着这件事来的:给一段完成特定任务的代码起个名字,打包成一个喊一声就能执行的单元。本篇只讲清四件事——怎么定义方法、参数怎么把外面的值递进去、返回值怎么把结果带回来、以及调用发生时程序按什么顺序走。

方法长什么样

1public static int max(int a, int b) {
2    if (a > b) {
3        return a;
4    }
5    return b;
6}

逐块拆开看:

  • public static 是修饰符,现在照抄就行;它们的含义等学完“类与对象”再回来说。
  • int 是 返回类型,说明这个方法会交回一个整数。
  • max 是方法名。
  • (int a, int b) 是 参数列表。每个参数都得写清类型和名字,用逗号隔开;没有参数就写一对空括号。
  • { … } 是 方法体,里面可以写任何你学过的语句——if、for 都可以(上面就用了上一篇学的 if)。

Oracle 官方教程的意思很直接:方法声明里只有四样东西是必需的——返回类型、方法名、一对圆括号、花括号包住的方法体 1。注意方法名后面那对括号不是装饰,它是“这是个方法”的标志。

参数:外面把值递进去

定义里的 int a、int b 叫 形参(parameter),它们是两个只存在于方法内部的变量。调用时写 max(x, y),括号里的 x、y 叫 实参(argument),是真实的、当场算出来的值。Oracle 把两者的区别说得很清楚:参数是声明里的那串变量,实参是调用时真正传进去的值 3。

两条硬规则:数量和类型必须对上,顺序也必须对上——max(3, 7) 和 max(7, 3) 都能编译,结果却完全不同。

更关键的一条是:Java 传参是 按值传递(pass by value),也就是把实参的值 复制一份 交给形参。

1public static void addOne(int n) {
2    n = n + 1;
3}
4
5public static void main(String[] args) {
6    int a = 5;
7    addOne(a);
8    System.out.println(a);   // 打印 5,不是 6
9}

调用 addOne(a) 时,Java 新建了一个变量 n,把 a 的值 5 复制进去。方法里把 n 改成 6,改的是这个副本,外面的 a 一点没动。Oracle 的说法是:基本类型参数按值传入,对参数的任何修改只存在于方法内部,方法返回后这些修改就丢失了 3。

这条机制顺带解释了:方法为什么需要返回值。既然它改不动外面的变量,想把算出来的结果送出去,就只能靠 return。

返回值:方法把结果带回来

有结果要送出,就先在声明里写清它是哪种类型,再在方法体里用 return 把值交出去。max 的返回类型是 int,所以 max(3, 7) 这个调用写完就是 一个 int 值,可以直接赋给变量:

1int bigger = max(3, 7);   // bigger 得到 7

return 有两件事同时发生:把值交出去;立刻结束整个方法。max 里一旦执行 if (a > b) return a;,后面那句 return b; 就根本不会跑到了,写在 return 之后的语句也永远轮不到执行。

这也带来一条编译规则:非 void 的方法必须保证 每一条可能的路径 上都走到 return,否则编译器直接报错 2。写得再正确也不行——只要编译器认为存在一条走到底却没有 return 的路径,就不放行。

类型也必须对得上:声明返回 int 就不能返回 boolean;反过来,声明为 void 的方法不允许 return 一个值,void 里写 return; 只表示“提前收工” 2。

不需要结果的方法就写 void。比如把上一篇的 for 循环整套搬进方法体:

1public static void printStars(int n) {
2    for (int i = 0; i < n; i++) {
3        System.out.print("*");
4    }
5    System.out.println();
6}

调用时,程序到底怎么走

拿这段调用当例子:

1public static void main(String[] args) {
2    int x = 3;
3    int y = 7;
4    int bigger = max(x, y);
5    System.out.println(bigger);
6}

执行顺序是这样:

  1. 走到 max(x, y),先算实参:x 的值 3、y 的值 7。实参可以是一个表达式,先算完再传。
  2. 控制权交给 max 的方法体,同时为 这一次调用 准备一套全新的变量空间:a 是 3,b 是 7。
  3. 方法体按顺序执行:3 > 7 为 false,跳过第一个 return;执行 return b;,把 7 交出去。
  4. 这一步做完,max 这次调用用的变量(a、b)全部销毁,控制权回到调用点,7 正好落在原来写 max(x, y) 的位置上。
  5. 继续执行调用点后面那一行:7 赋给 bigger。
绘制中

第 2 步和第 4 步是重点。JVM 规范把每次调用对应的这块空间叫 栈帧(frame),里面装着这次调用的参数和局部变量;方法一返回,这块空间就整块丢掉 4。正因为是“新建一套”,方法里叫 a、外面叫 x,两个变量互不相干;也正因为如此,同一个方法在循环里被调用 100 次,每次拿到的都是一套干净的新变量。

还有一个容易忽略、但非常重要的推论:方法在哪里被调用,执行完就回到哪里。max 完全不知道调用它的那行代码写在 main 里、写在某个循环里,还是写在另一个方法里——它只负责把值交出去,剩下的交给调用点继续。

几个新手坑

非 void 方法漏了 return。 编译器报 missing return statement。判断标准是“所有路径”而不是“看起来会走到”:if 里 return 了,但没写 else,编译器仍然认为存在一条走到底的路。

返回类型对不上。 声明 int 却 return 一个 double 或 boolean,编译不过 2。

以为改形参外面会跟着变。 前面那个 addOne 已经演示过:基本类型形参是副本。

调用时忘了写括号。 只有 max(3, 7) 才是调用;光写一个 max,那只是方法的名字,程序不会执行它,编译器也会因为找不到这个变量而报错。

小结与预告

方法就是四件事:声明时写清返回类型、方法名、参数类型;调用时实参的个数、类型、顺序都要对上;形参是副本,在里面怎么改都影响不到外面;结果通过 return 交回,非 void 方法必须保证每条路径都有 return。

预告一句:名字相同、参数类型不同的两个方法可以同时存在,调用时 Java 按你给的实参挑一个执行,这叫作 重载(overloading),下一篇专门讲它的规则。