深入 Java 继承:从 Object 到模板方法,再到组合与 final 的设计抉择

46 阅读2分钟

一、继承的本质:消灭重复代码
面向对象三大特性——封装、继承、多态——面试常问,却少有人一语道破继承的本质:代码复用。

public class Dog extends Animal {
    public void wang() { }
}

Dog 一旦 extends Animal,就天然获得了父类所有非 private 的能力,无需 Ctrl+C / Ctrl+V。

二、Java 单根体系与 Object 三大常见覆写
所有类的终极父类是 java.lang.Object,因此任何对象都至少有:

  1. equals:默认比较地址,实际业务几乎必覆写。
  2. hashCode:与 equals 一起重写,保证在 HashMap/HashSet 中行为一致。
  3. toString:给调试日志一个可读字符串。

以 Order 为例:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    return id.equals(((Order) o).id);
}

@Override
public int hashCode() {
    return Objects.hash(id);
}

IDE 一键生成即可,手写既繁琐又易错。

三、== 与 equals 的区别
• == 比较“栈里的值”,对于对象即地址。
• equals 比较“业务意义上的内容”,可由我们自行定义。

四、类加载与初始化顺序
严格自顶向下:父类静态 → 子类静态 → 父类构造器 → 子类构造器。
如果父类没有无参构造,子类必须在第一行显式 super(args),否则编译失败。

五、实例方法的覆写(Override)

@Override
public void writeContent() {
    super.writeContent(); // 想保留父逻辑再扩展
    System.out.println("我的内容");
}

@Override 只是一个让编译器帮咱把关的注解,写错方法签名即报错。

六、模板方法设计模式实战
骨架在父类,变化点下沉到子类:

public abstract class BookWriter {
    public final void writeBook() { // 模板
        writeTitle();
        writeContent();
        writeEnding();
    }
    protected abstract void writeContent(); // 变化
}

Spring 的 refresh() 也是同理。

七、向上转型、向下转型与 instanceof
向上转型安全且自动:

Animal a = new Cat("Tom"); // 总是 OK

向下转型必须强转,可能抛 ClassCastException,先用 if (a instanceof Cat) 守护。

八、final 关键字的四种玩法

  1. 变量:值或引用不可变。
  2. 参数:禁止在方法体内重新赋值。
  3. 方法:禁止子类覆写。
  4. 类:禁止继承,如 final class World {}
    带来的好处:
    • 线程安全(不可变对象天然并发友好)
    • 运行期可内联,提升性能
    • 设计层面“关闭修改”,避免子类破坏父类约定。

九、单例模式与 final 结合

public final class World {
    public static final World INSTANCE = new World();
    private World() { }
}

私有化构造器 + 公共静态常量 = 最简单线程安全单例。

十、组合优于继承
“is-a” 才用继承,“has-a” 用组合。
错误示范:继承 HashSet 实现 CountingSet,因父类 addAll 内部会调 add,导致计数翻倍。
正确做法:

public class CountingSet {
    private final Set<Object> set = new HashSet<>();
    private int count;
    public boolean add(Object o) {
        count++;
        return set.add(o);
    }
}

组合不会暴露父类细节,耦合度更低,测试也更容易。