第 6 章 · 方法

方法(其他语言叫函数)是把代码变成积木的手段:写一次、调百次。这一章除语法外,有三块硬骨头值得花力气:Java 的传参模型(只有值传递,但对象能被改,这句话的完整解释在本章)、重载(同名不同参,JVM 怎么选)、递归(数据结构课第 9 章的前置)。学完这章,回头再看第 1 章那个 public static void main,每个单词就都能说清楚了。

6.1 方法的解剖:main 的每个单词

public static int max(int a, int b) {   // 修饰符 返回类型 名字(参数)
    return a > b ? a : b;
}

用这个最简例子把第 1 章的 main 拆掉:public 谁都能调(访问权限,第 7 章细讲),static 属于类不属于对象(不 new 就能调,第 7 章),int 返回值类型(void 表示不返回),max 名字,(int a, int b) 参数表。方法签名 = 名字 + 参数列表(返回值不参与签名),这是理解重载的钥匙。

return 的两条规则:返回类型非 void 时所有路径必须有 return(编译器强制,漏一条编译不过);return 即刻结束方法(可以拿它提前退出错误分支,配合出参就是 C 课第 10 章讲过的模式)。

6.2 传参:只有值传递,但对象能被改

Java 的传参规则一句话:拷贝实参的值。微妙处在于「值」对不同类型含义不同:

static void changePrimitive(int x) { x = 99; }          // 改复印件,无效
static void changeArray(int[] arr) { arr[0] = 99; }     // 改数组内容,有效!
static void reassign(int[] arr) { arr = new int[5]; }   // 改复印件(门牌),无效

int[] a = {1, 2, 3};
changePrimitive(a[0]);    // a[0] 还是 1
changeArray(a);           // a[0] 变 99
reassign(a);              // a 还是原来那个数组

完整解释(指针课第 10 章同款逻辑):基本类型的值就是数据本身,拷贝过去怎么改都碰不到原件;引用类型的值是「对象的门牌」,拷贝门牌过去,顺着门牌能改到同一个对象(changeArray 生效),但把复印件改成新门牌(reassign)只影响方法内的变量。所以那句经典总结的正确读法是:「Java 按值传递一切;引用类型的值恰好是地址,于是能共享内容」。C++ 的 int& 真引用、Python 的可变对象共享,都是同一原理的不同包装,面试题「Java 到底是值传递还是引用传递」的标准答案就藏在 6.2 这一段。

想要「改基本类型的实参」怎么办?和 C 同款答案:包装成对象/数组,或用返回值(int[] maxAndMin 返回两个结果),Java 没有 C 的出参指针风格,但有第 7 章的字段可改。

6.3 重载:同名不同参

static int    abs(int x)    { return x < 0 ? -x : x; }
static double abs(double x){ return x < 0 ? -x : x; }
static long   abs(long x)   { return x < 0 ? -x : x; }

abs(-5);        // 编译器按参数类型选第一个
abs(-5.5);      // 选第二个

重载(overload)= 方法名相同、参数列表不同(个数或类型),编译器按实参类型静态决定调谁。三条边界:返回值不同不算重载(签名不含返回值,int f() 和 double f() 共存编译错误);自动类型提升会参与匹配(传 int 给 abs(double) 也行,选最具体的);歧义参数(f(1, 2) 面对 f(int, double) 和 f(double, int))直接编译错误。System.out.println 能打印万物,就是因为它重载了十几个版本,你已经用了五章的「多态」其实是重载。

变长参数 int... nums 本质是数组参数的糖:

static int sum(int... nums) {          // 调用 sum(1,2,3) 或 sum()
    int s = 0;
    for (int n : nums) s += n;
    return s;
}

6.4 递归与作用域

递归在数据结构课第 9 章已经系统讲过(栈帧成本、递推式、何时放弃递归),这里补 Java 特有的两点:JVM 默认栈512KB~1MB(比 Linux C 的 8MB 小,-Xss 可调),递归深度上限更敏感;斐波那契朴素递归的指数爆炸在 Java 同样存在,记忆化(HashMap 缓存)是第 9 章集合类的作业。作用域规则回顾:局部变量声明处生效、必须初始化;成员变量有默认值;同名时局部遮蔽成员,this.x 显式指成员(第 7 章的主角登场前,先记住这个语法)。

常见错误

  • 以为「传数组能改,那传 int 也该能改」。6.2 的第一课,判据是「值是不是地址」。
  • 重载只改返回值。编译错误「method already defined」,签名不含返回值。
  • 递归忘终止条件。StackOverflowError,JVM 级别的爆栈(数据结构课第 9 章讲过机制),报错会打印上千行递归栈,从栈里数重复帧能立刻定位。
  • 方法写在类外面。Java 强制一切方法属于类(C 的自由函数不存在),「单独写个工具函数」也得挂个类(第 7 章的 static 工具类惯例)。

练习

  1. 动手:验证 6.2 的三种情况(基本类型、改内容、改引用),打印方法调用前后的值,用自己的话写一句「Java 传参模型」的总结。
  2. 实现:重载一组 print(分别接收 int、double、String、int[]),再实现变长参数版 printAll(Object...)(第 8 章 Object 再回头理解,现在照抄)。
  3. 实现:汉诺塔(数据结构课第 9 章),Java 版;故意不写终止条件,观察 StackOverflowError 的报错形态,数一数栈里有多少层重复帧。
  4. 思考:Java 方法必须属于类,而 C/C++ 有自由函数。这个强制换来了什么(包结构、类库组织、命名空间管理),又造成什么小别扭(工具方法要挂类、static 满天飞)?结合你用过的 Math.max、Arrays.sort(都是 static 方法),评价这个设计。