继承:从入门到JVM底层,一篇搞定

继承是OOP三大特征中最具争议的一个。

爱它的人说:继承让代码复用变得极其简单,子类可以"免费"获得父类的所有功能。

恨它的人说:继承破坏了封装,让代码变得脆弱,一改父类、子类全崩。

真相是:继承是一把锋利的刀,用好了切菜如泥,用不好砍到自己脚。

一、继承到底在表达什么?

1.1 is-a关系

继承就干一件事:表达"是一种"。

  • 是一种 动物
  • 卡车 是一种 汽车

判断标准:你能对着代码说出口"子类对象是父类类型"?业务上说得通,就用继承。

class Animal { }
class Dog extends Animal { }  // Dog is an Animal ✓

1.2 什么时候别用继承?

汽车有引擎——这不是"是一种",是"有一个"(has-a)。

// ✗ 错误:汽车不是引擎
class Engine { }
class Car extends Engine { }

// ✓ 正确:汽车有一个引擎
class Car {
    private Engine engine;
}

一句话原则:is-a用继承,has-a用组合。

二、语法速览

// 父类
public class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public void eat() {
        System.out.println(name + "正在吃东西");
    }
}

// 子类
public class Dog extends Animal {
    private String breed;
    
    public Dog(String name, String breed) {
        super(name);  // 不写这行编译报错
        this.breed = breed;
    }
    
    @Override  // 推荐加上,让编译器帮你检查
    public void eat() {
        System.out.println(name + "正在啃骨头");
    }
}

@Override不是必须的,但强烈推荐。如果你写成了eatt(),不加注解编译器认为你写了新方法,加上注解会直接报错。

三、底层原理:new一个子类对象,内存里发生了什么?

3.1 对象的真实内存布局

Dog dog = new Dog("旺财", "金毛");

JVM在堆中分配的内存结构:

┌─────────────────────────────────────────────────┐
│                  Dog对象(堆内存)                 │
├─────────────────────────────────────────────────┤
│                   对象头(12/16字节)              │
│  - Mark Word(哈希码、GC分代年龄、锁状态)          │
│  - Klass Pointer → 指向Dog的类元数据              │
├─────────────────────────────────────────────────┤
│                 实例数据(对齐填充)                │
│  ┌─────────────────────────────────────────────┐ │
│  │  父类部分(Animal的字段)                     │ │
│  │  name = "旺财"(引用,4/8字节)               │ │
│  └─────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────┐ │
│  │  子类部分(Dog的字段)                        │ │
│  │  breed = "金毛"(引用,4/8字节)              │ │
│  └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

三个关键点:

  1. 子类对象里确实有父类的所有字段(包括private的,只是你访问不到)
  2. 初始化顺序固定:父类构造器先执行 → 子类构造器后执行
  3. Klass Pointer是多态实现的底层基础

3.2 类加载阶段:继承链的解析

当JVM加载Dog类时,会递归加载其父类:

1. 加载Animal类 → 创建Animal的类元数据
2. 加载Dog类 → 创建Dog的类元数据,其中包含指向Animal类元数据的super指针
3. 构建虚方法表(vtable)

3.3 方法调用底层:虚方法表(vtable)

Animal animal = new Dog("旺财", "金毛");
animal.eat();  // 输出:旺财正在啃骨头

字节码层面:

invokevirtual #4    // Method Animal.eat:()V

invokevirtual指令的执行步骤:

  1. 通过对象头的Klass Pointer找到Dog的类元数据
  2. 从类元数据中找到虚方法表
  3. 在方法表中查找eat()的实际地址(偏移量固定,查找是O(1))
  4. 执行Dog.eat()

虚方法表示例:

偏移量 方法签名 Animal的方法表 Dog的方法表
0 eat() Animal.eat() Dog.eat() ← 覆盖
1 sleep() Animal.sleep() Animal.sleep() ← 继承
2 toString() Object.toString() Object.toString()

性能说明:虚方法调用比静态方法多一次内存间接寻址,但JIT会做内联优化(如去虚化),实际开销极小。

3.4 super是什么?

很多人以为super是父类对象的引用。不是

super编译器指令

class Dog extends Animal {
    void test() {
        super.eat();
    }
}

反编译后:

invokespecial #2    // Method Animal.eat:()V

invokespecial直接绑定父类方法,不经过动态分派。这就是为什么super.eat()不会产生多态效果。

3.5 完整初始化时序图

new Son() 执行流程:

时间轴 →
┌─────────────────────────────────────────────────────────────────────┐
│ 类加载阶段(仅一次)                                                  │
│  ├── 加载Object类 → 加载Father类 → 加载Son类                         │
│  └── 构建虚方法表                                                    │
├─────────────────────────────────────────────────────────────────────┤
│ 实例化阶段                                                           │
│  ├── 1. 为Father+Son所有字段分配内存(包括默认值0/false/null)        │
│  ├── 2. 执行Father实例变量初始化:a = 1                               │
│  ├── 3. 执行Father构造器 → 调用print()(此时子类c还是0!)            │
│  ├── 4. 执行Son实例变量初始化:c = 3                                  │
│  └── 5. 执行Son构造器 → 调用print()(此时c=3)                        │
└─────────────────────────────────────────────────────────────────────┘

四、三个必踩的坑(附解决方案)

坑一:父类一改,子类就崩(脆弱的父类问题)

// 父类
public class Counter {
    private int count = 0;
    public void increment() { count++; }
    public void incrementTwice() {
        increment();
        increment();
    }
}

// 子类
public class LoggingCounter extends Counter {
    @Override
    public void increment() {
        System.out.println("increment called");
        super.increment();
    }
}

某天父类优化为:

public void incrementTwice() {
    count += 2;  // 不再调用increment()
}

子类的日志逻辑静默失效——没有任何编译错误,运行时也不会抛异常,只是日志不打印了。

解决方案:

  • 父类中可能被子类重写的方法,用文档明确契约(@implSpec
  • final禁止重写关键方法
  • 或使用组合替代继承

坑二:正方形继承矩形(违反里氏替换)

class Rectangle {
    protected int width, height;
    public void setWidth(int w) { width = w; }
    public void setHeight(int h) { height = h; }
    public int getArea() { return width * height; }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        width = w;
        height = w;
    }
    @Override
    public void setHeight(int h) {
        height = h;
        width = h;
    }
}

void test(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    System.out.println(r.getArea());  // 期望20
}

test(new Square());  // 输出16 ✗

解决方案: 让正方形和矩形都继承更抽象的Shape类,各自实现。

坑三:继承层次过深

AnimalMammalCanineDogRetrieverGoldenRetriever

问题:

  • 理解一个类的行为需要翻6个类
  • 顶层修改影响所有下层
  • 测试成本指数增长

经验法则:继承层次不超过3层。

五、JDK源码中的继承设计

5.1 好的设计:AbstractList → ArrayList

// 模板方法模式
public abstract class AbstractList {
    public boolean add(E e) {
        add(size(), e);  // 调用抽象方法
        return true;
    }
    public abstract void add(int index, E element);
}

public class ArrayList extends AbstractList {
    @Override
    public void add(int index, E element) {
        // 具体实现
    }
}

父类定义了算法骨架(模板方法),子类只实现变化的部分。

5.2 差的设计:Stack extends Vector

// Java早期设计失误
class Stack<E> extends Vector<E> {  // 不该继承Vector
    public void push(E item) {
        addElement(item);
    }
}

问题:Stack本应是LIFO,但继承了Vector的所有方法,可以绕开规则直接在中间插入元素。

正确做法(组合):

class Stack<E> {
    private List<E> elements = new ArrayList<>();
    
    public void push(E item) {
        elements.add(item);
    }
    
    public E pop() {
        if (isEmpty()) throw new EmptyStackException();
        return elements.remove(elements.size() - 1);
    }
}

六、继承 vs 组合:决策框架

6.1 组合长什么样?

class Engine {
    void start() { }
}

class Car {
    private Engine engine;  // 组合
    
    Car() {
        engine = new Engine();
    }
    
    void start() {
        engine.start();  // 委托
    }
}

6.2 决策三问

问题
能说通"子类是父类的一种"? → 考虑继承 → 用组合
子类能完全替代父类(里氏替换)? → 考虑继承 → 用组合
父类是否会频繁变化? → 用组合 → 考虑继承

三个都是"是",且需要多态,才用继承。

6.3 组合+接口的进阶模式(推荐)

// 接口定义能力
interface Flyable {
    void fly();
}

// 组合 + 委托
class Bird implements Flyable {
    private FlyBehavior flyBehavior;  // 组合
    
    Bird(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }
    
    @Override
    public void fly() {
        flyBehavior.fly();  // 委托
    }
}

这比继承更灵活:飞行行为可以运行时切换。

七、完整实战:员工工资系统

public abstract class Employee {
    private String id;
    private String name;
    private double baseSalary;
    
    public Employee(String id, String name, double baseSalary) {
        this.id = id;
        this.name = name;
        this.baseSalary = baseSalary;
    }
    
    public String getName() { return name; }
    protected double getBaseSalary() { return baseSalary; }
    
    public abstract double calculateSalary();
    
    public void work() {
        System.out.println(name + "正在工作");
    }
}

public class RegularEmployee extends Employee {
    private double attendanceBonus;
    
    public RegularEmployee(String id, String name, double baseSalary, double attendanceBonus) {
        super(id, name, baseSalary);
        this.attendanceBonus = attendanceBonus;
    }
    
    @Override
    public double calculateSalary() {
        return getBaseSalary() + attendanceBonus;
    }
    
    @Override
    public void work() {
        System.out.println(getName() + "正在处理业务");
    }
}

public class Manager extends Employee {
    private double teamBonus;
    private int teamSize;
    
    public Manager(String id, String name, double baseSalary, double teamBonus, int teamSize) {
        super(id, name, baseSalary);
        this.teamBonus = teamBonus;
        this.teamSize = teamSize;
    }
    
    @Override
    public double calculateSalary() {
        return getBaseSalary() + teamBonus * teamSize;
    }
    
    @Override
    public void work() {
        System.out.println(getName() + "正在管理团队");
    }
}

八、面试高频题(含陷阱)

Q1:Java为什么不能多继承类?

菱形继承问题:C继承A和B,A和B都有m(),调用c.m()冲突。

Java 8后接口有默认方法,处理规则:

  1. 类的方法优先于接口默认方法
  2. 子接口优先于父接口
  3. 冲突必须显式覆盖
interface A { default void m() {} }
interface B { default void m() {} }
class C implements A, B {
    @Override
    public void m() { A.super.m(); }  // 必须自己选
}

Q2:构造方法为什么不能被继承?

  1. 语法:构造方法名必须和类名相同
  2. 语义:子类通过super()初始化父类部分就够了

Q3:重写和重载的区别?

维度 重写 重载
位置 子类和父类之间 同一个类
参数 必须相同 必须不同
返回类型 协变(可返回子类) 可以不同
访问权限 不能更严格 无关
绑定时机 运行时(动态分派) 编译时(静态分派)

Q4:初始化的完整顺序(高频陷阱题)

class Father {
    int a = 1;
    Father() { print(); }
    void print() { System.out.println(a); }
}

class Son extends Father {
    int c = 3;
    Son() { print(); }
    void print() { System.out.println(c); }
}

new Son();  // 输出?

答案:0 3

为什么第一个是0? 因为父类构造器执行时,子类的实例变量c内存已分配但尚未执行初始化(默认值为0),而子类的print()方法被调用时访问的就是这个未初始化的c

完整顺序:

  1. 父类静态
  2. 子类静态
  3. 父类实例变量 → 父类构造器(此时子类c默认0)
  4. 子类实例变量 → 子类构造器

Q5:static方法能被重写吗?

不能。只能叫"隐藏"。

class Parent {
    static void m() { System.out.println("Parent"); }
}
class Child extends Parent {
    static void m() { System.out.println("Child"); }
}

Parent p = new Child();
p.m();  // 输出"Parent",不是"Child"

invokestatic指令在编译时就确定了调用目标,不经过动态分派。

Q6:抽象类为什么可以有构造方法?

抽象类不能new,但它的构造方法是给子类用的——子类实例化时先调用父类构造器初始化父类部分。

Q7:如何防止被继承?

  • final class:无法被继承
  • private构造器 + 静态工厂方法
  • sealed class(Java 17+):限制哪些类可以继承

九、IDE实用技巧

操作 IDEA快捷键
查看类继承树 Ctrl+H / Ctrl+Alt+U
跳转到父类方法 Ctrl+U
查看方法重写 Ctrl+O
生成重写方法 Ctrl+O(在子类中)

十、总结

继承的核心不在于"子类能做什么",而在于"父类约束了什么"。

三条实用原则:

  • 继承表达is-a,不是has-a
  • 组合优先于继承,但不等于永远不用继承
  • 只有真正满足"子类能完全替代父类"时才用继承

一句话记住:

继承是契约,不是偷懒;能说is-a,还要能替换。

面试官想听的,不是你背出来的概念,而是你能说出**“为什么这样设计"以及"什么场景下不能这样用”**。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐