上一篇我们用循环解决了“在同一个地方重复执行”。但循环只能在它所在的位置一遍遍转圈。真正的麻烦是:同一段逻辑要在程序的好几个地方用,甚至要在循环里一次次被调用。把代码复制三遍能跑,可一旦要改,就得同时改三处,改漏一处就出 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 得到 7return 有两件事同时发生:把值交出去;立刻结束整个方法。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}执行顺序是这样:
- 走到
max(x, y),先算实参:x的值 3、y的值 7。实参可以是一个表达式,先算完再传。 - 控制权交给
max的方法体,同时为 这一次调用 准备一套全新的变量空间:a是 3,b是 7。 - 方法体按顺序执行:
3 > 7为 false,跳过第一个return;执行return b;,把 7 交出去。 - 这一步做完,
max这次调用用的变量(a、b)全部销毁,控制权回到调用点,7 正好落在原来写max(x, y)的位置上。 - 继续执行调用点后面那一行: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),下一篇专门讲它的规则。