第 3 章欠的账现在还:为什么 "hello" == "hello" 有时 true、new String("hello") == "hello" 却 false?答案在 Object 类的 equals 方法,而要讲清 equals 就必须先讲继承和多态。本章是面向对象的主菜:继承、抽象类与接口、多态(整个 Java 世界的运转核心)、异常体系、包与访问控制。学完它,你才算真的在写 Java。
class Animal {
protected String name;
void eat() { System.out.println(name + " eating"); }
}
class Dog extends Animal {
void bark() { System.out.println("wang!"); }
@Override
void eat() { super.eat(); System.out.println("(tail wagging)"); } // 重写
}
Dog d = new Dog();
d.eat(); // 先走 Dog 的 eat:调 super.eat() 再补一句
Dog 拿到 Animal 的全部字段和方法(is-a 关系),又用**重写(@Override)**改造了 eat。三条纪律:@Override 注解必须写(拼错方法名时编译器立刻报错,没有它你重写了个寂寞);super.eat() 调父类版本(重写后想保留父类逻辑的标准姿势);子类构造必须先调父类构造(编译器自动补 super(),父类没有无参构造时你得显式写)。Java 单继承(只有一个直接父类),所有类的根是 Object,单继承 + 多接口的组合是它对 C++ 菱形继承问题的回应。
final 顺手清掉:类(不可被继承,如 String)、方法(不可被重写)、变量(不可再赋值;注意引用 final 只是门牌不变,房子里的家具照样能换,final int[] a 里的 a[0] 照改)。
抽象类:有抽象方法(只有签名没有实现)的类,不能实例化,子类要么补全要么继续抽象:
abstract class Shape {
protected String name;
abstract double area(); // 强制子类给答案
void describe() { System.out.println(name + " 面积 " + area()); } // 模板方法
}
接口:纯规范,一撮方法的合同,JDK 8 后可以有 default 方法:
interface Comparable2 {
int compareTo(Object o);
default boolean biggerThan(Object o) { return compareTo(o) > 0; } // 白送的方法
}
class Circle extends Shape implements Comparable2 { ... } // 单继承 + 多接口
选型口诀:抽象类是「是什么」(共享的代码和状态,血缘),接口是「能做什么」(纯能力,合同)。Bird is-a Animal extends 它;Bird can-do Flyable implements 它。一个类只能 extends 一个父类,但能 implements 一把接口,Java 把「代码复用」放在继承、「行为合同」放在接口,各司其职。Comparable/Runnable 这些标准库接口,你后面章节天天用。
面向对象的灵魂,一例胜千言:
Shape[] shapes = { new Circle(2), new Rect(3, 4) };
for (Shape s : shapes)
System.out.println(s.describe()); // 各自调各自的 area()!调用方完全不知道具体类
s.describe() 这一行代码,对 Circle 和 Rect 走了不同的 area 实现,这叫动态绑定:调用哪个方法由对象的实际类型在运行时决定,而不是变量声明类型。机器级的实现是方法表(vtable,计组课第 11 章函数指针表的 oo 版):每个类一张表,重写过的槽位换成子类版本,调用 = 查表 + 间接调用(第 11 章讲过的「间接调用税」)。多态的工程价值:新增三角形只需写 Tri extends Shape,describe 那段循环一个字不改(对扩展开放,对修改关闭)。
配套两件套:向上转型自动(Shape s = new Circle(),安全),向下转型要 instanceof 先问:
if (s instanceof Circle c) { // JDK 16+ 的模式匹配写法,判断+转型一行
c.radius = 5;
}
频繁 instanceof 通常是设计问题的信号(该用多态的地方没用上),偶发的类型收窄才合理。
所有类都继承 Object,三个方法几乎总该重写:
class Point {
int x, y;
@Override public boolean equals(Object o) {
if (this == o) return true; // 自反速查
if (!(o instanceof Point p)) return false; // 类型不对直接 false
return x == p.x && y == p.y; // 逐字段比内容
}
@Override public int hashCode() { return 31 * x + y; } // 与 equals 一致
@Override public String toString() { return "(" + x + "," + y + ")"; }
}
这就是第 3 章 == 之谜的谜底:String 重写了 equals(比内容),而 == 永远比引用;"hello" 是字面量常量(池化复用,同一对象,== 碰巧 true),new String 是新对象(必 false)。比较内容永远 equals 的纪律,根源在此。hashCode 的规则也钉一下:equals 相等则 hashCode 必须相等(反之不必),这条契约是第 9 章 HashMap 能正常工作的前提,重写了 equals 忘写 hashCode,HashMap 里存的对象会「找不到」。
Java 把运行期错误做成对象抛出,沿调用栈向上传播,中途可接住:
try {
int r = 10 / 0; // 抛 ArithmeticException
} catch (ArithmeticException e) {
System.out.println("除零:" + e.getMessage()); // 接住,程序不死
} finally {
System.out.println("无论如何都执行"); // 收尾(关资源)
}
异常分两族:受检异常(IOException 这类,编译器强制你 catch 或 throws 声明,「要么处理要么声明甩锅」)、非受检异常(RuntimeException 家族:NPE、数组越界、除零,编译器不管,代表「编程错误」)。设计哲学:受检管「外部世界的不确定」(文件不存在、网络断),非受检管「程序自身的 bug」(空指针不该 catch,该修代码)。自定义异常继承 Exception(受检)或 RuntimeException(非受检),带个 message 构造就够用。和 C 的返回码 + errno(指针课第 10 章)相比,异常把「正常逻辑」和「错误处理」分开了,代价是控制流变得不可见(一行代码可能突然跳到很远),try 里只包必要的范围是纪律。
包(package)= 目录 = 命名空间,import java.util.Scanner 的 java.util 就是包名。访问修饰符四级,从窄到宽:private(本类)、默认(同包)、protected(同包加子类)、private(本类)< 默认(同包)< protected(同包 + 子类)< public(全世界)。工程惯例:字段 private,方法按需开,跨包要用的 public,同包协作的默认。第 1 章「一个文件一个 public 类」的规矩现在能解释了:public 类要被外部 import,同文件的非 public 类只服务同包,门卫等级决定的。
String a = "he" + "llo"; String b = "hello"; 打印 a == b(编译期常量折叠,true!);换成 String c = new String("hello") 再比。用常量池解释三种结果。InsufficientBalanceException,在 BankAccount(第 7 章的)withdraw 余额不足时抛出,catch 它并打印信息。