继承
本节定位:继承是面向对象三大特性之一。本节从
extends关键字出发,深入 Java 单继承限制的设计考量、构造方法链中的隐式super()调用、Object 根父类的角色,以及方法重写的完整规则与底层虚方法表机制。
为什么需要继承
飞翔科技技术部的实习生小崔在开发员工管理系统时,写出了这样的代码:
// 操作前:Manager.java —— 大量字段与 Employee 重复
public class Manager {
private String name;
private String id;
private double salary;
private double bonus; // 独有
public String getName() { return name; }
public String getId() { return id; }
public double getSalary() { return salary + bonus; }
// ... 构造方法、setter 等
}
// Developer.java —— 同样重复 name / id / salary
public class Developer {
private String name;
private String id;
private double salary;
private String programmingLanguage; // 独有
// ... 重复的 getter/setter
}
架构师白歌在 code review 时指出:"这就是典型的重复代码坏味道。Manager 和 Developer 都是员工——它们共享 name、id、salary 属性和行为。应该抽取一个 Employee 基类。"
extends 关键字
基本语法
// 定义父类(基类 / 超类)
[修饰符] class 父类名 {
// 成员变量和方法
}
// 定义子类(派生类)
[修饰符] class 子类名 extends 父类名 {
// 子类新增的成员变量和方法
}
语法表
| 要素 | 说明 | 示例 |
|---|---|---|
extends | 关键字,表示继承关系 | class Dog extends Animal |
| 父类 | 被继承的类,也称基类、超类 | Animal |
| 子类 | 继承父类的类,也称派生类 | Dog |
| 继承内容 | 子类继承父类所有非 private 成员 | 成员变量、方法 |
| 不继承内容 | 构造方法、private 成员 | — |
操作后:用继承重构飞翔科技员工系统
// ✅ 正确:抽取公共基类 Employee
public class Employee {
private String name;
private String id;
private double salary;
public Employee(String name, String id, double salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
// --- getter / setter ---
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public double getSalary() { return salary; }
public void setSalary(double salary) { this.salary = salary; }
// 工作方法(子类可重写)
public String work() {
return name + " 正在工作中...";
}
// 计算年薪(子类可重写以实现不同的薪资结构)
public double computeAnnualSalary() {
return salary * 12;
}
}
// Manager 继承 Employee
public class Manager extends Employee {
private double bonus; // 经理独有:奖金
public Manager(String name, String id, double salary, double bonus) {
super(name, id, salary); // 调用父类构造方法
this.bonus = bonus;
}
public double getBonus() { return bonus; }
public void setBonus(double bonus) { this.bonus = bonus; }
@Override
public String work() {
return "经理 " + getName() + " 正在制定项目计划...";
}
@Override
public double computeAnnualSalary() {
return getSalary() * 12 + bonus;
}
}
// Developer 继承 Employee
public class Developer extends Employee {
private String programmingLanguage; // 开发者独有
public Developer(String name, String id, double salary, String programmingLanguage) {
super(name, id, salary);
this.programmingLanguage = programmingLanguage;
}
public String getProgrammingLanguage() { return programmingLanguage; }
@Override
public String work() {
return "开发者 " + getName() + " 正在用 " + programmingLanguage + " 编写代码...";
}
@Override
public double computeAnnualSalary() {
return getSalary() * 12; // 开发者 12 薪,无奖金
}
}
// 测试类
public class InheritanceTest {
public static void main(String[] args) {
Manager m = new Manager("大翔", "E001", 50000, 200000);
Developer d = new Developer("小崔", "E002", 25000, "Java");
System.out.println(m.work()); // 经理 大翔 正在制定项目计划...
System.out.println(m.computeAnnualSalary());// 800000.0
System.out.println(d.work()); // 开发者 小崔 正在用 Java 编写代码...
System.out.println(d.computeAnnualSalary());// 300000.0
}
}
Java 单继承限制
Java 规定一个类只能有一个直接父类(单继承),但可以多层继承。
为什么 Java 不支持多继承?
Java 设计者为了避免 C++ 多继承带来的"钻石问题"(Diamond Problem):如果两个父类有同名方法,子类不知道调用哪一个。
// ❌ Java 不允许(编译错误)
// class X extends A, B { } // 语法错误
// ✅ Java 通过接口实现多继承效果(第10章)
class X implements A, B { } // 接口多继承
白歌点评:单继承让类层次清晰可预测。Java 用接口(interface)弥补了多继承的功能需求,同时避免了语义歧义。这一设计是 Java 相比 C++ 的重要简化。
子类不能继承父类的 private 成员
public class Employee {
private String name; // private:仅本类可访问
public String getName() { return name; }
}
public class Manager extends Employee {
public void printName() {
// System.out.println(name); // ❌ 编译错误:name 是 private
System.out.println(getName()); // ✅ 通过 getter 访问
}
}
| 访问修饰符 | 本类 | 同包子类 | 不同包子类 | 同包非子类 | 全局 |
|---|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ | ❌ |
default | ✅ | ✅ | ❌ | ✅ | ❌ |
protected | ✅ | ✅ | ✅ | ❌ | ❌ |
public | ✅ | ✅ | ✅ | ✅ | ✅ |
关键结论:子类不继承父类的
private成员,但可以通过父类提供的public或protected的 getter/setter 间接访问。这体现了封装原则——父类控制哪些数据对外开放。
子类构造方法与 super() 隐式调用
规则
子类构造方法的第一行必须调用父类构造方法(显式或隐式)。
public class Manager extends Employee {
// 如果子类构造方法没有显式调用 super(),
// 编译器自动插入 super() —— 调用父类的无参构造方法
public Manager() {
// super(); ← 编译器隐式插入
}
}
如果父类没有无参构造方法,子类必须显式调用父类的有参构造方法。
// ❌ 错误示例
public class Employee {
private String name;
public Employee(String name) { this.name = name; } // 无无参构造
}
public class Manager extends Employee {
public Manager() { // ❌ 编译错误:父类无无参构造
// 隐式 super() 找不到匹配的构造方法
}
}
// ✅ 正确示例
public class Manager extends Employee {
public Manager(String name) {
super(name); // 显式调用父类有参构造
}
}
完整构造器链示例
public class InheritanceConstructorTest {
public static void main(String[] args) {
Manager m = new Manager("白歌", "E003", 40000, 150000);
System.out.println(m.getName()); // 白歌
}
}
构造方法执行顺序:
1. Object() 构造方法
2. Employee(String name, String id, double salary) 构造方法
3. Manager(String name, String id, double salary, double bonus) 构造方法
执行栈:
Manager() -> super() -> Employee() -> super() -> Object()
↓ ↓ ↓
this.bonus name/id/salary 最顶层
Object 类:所有类的根父类
Java 中所有类(包括数组)都直接或间接继承自 java.lang.Object。
| Object 方法 | 说明 |
|---|---|
toString() | 返回对象的字符串表示,默认:类名@十六进制hashCode |
equals(Object obj) | 判断两个对象是否"相等",默认比较引用地址(==) |
hashCode() | 返回对象的哈希码,配合 HashMap/HashSet 使用 |
getClass() | 返回对象的运行时类 |
clone() | 创建并返回对象副本(需实现 Cloneable 接口) |
finalize() | GC 回收前调用(JDK 9 已过时,不推荐使用) |
public class ObjectMethodTest {
public static void main(String[] args) {
Manager m = new Manager("孔蓝", "E004", 35000, 100000);
System.out.println(m.toString());
// 默认输出类似:com.feixiang.Manager@15db9742(未重写时)
System.out.println(m.getClass().getName()); // com.feixiang.Manager
}
}
方法重写(Override)
子类重新定义父类中已有的方法,称为方法重写。重写是实现多态的基础。
重写规则表
| 规则 | 要求 |
|---|---|
| 方法签名 | 方法名、参数列表必须完全一致 |
| 返回值类型 | JDK 5+ 支持协变返回:子类返回值可以是父类返回值的子类型 |
| 访问修饰符 | 子类方法访问权限不能比父类更严格(可以更宽松) |
| 异常 | 子类方法抛出的检查异常不能比父类更宽泛 |
| static 方法 | 不能被重写(可以被隐藏) |
| final 方法 | 不能被重写 |
| private 方法 | 不能被重写(子类不可见) |
@Override 注解
public class Manager extends Employee {
@Override // 编译器检查是否真的重写了父类方法
public String work() {
return "经理 " + getName() + " 正在管理团队";
}
}
@Override 的作用:
- 编译期检查:如果方法签名不匹配父类任何方法,编译器报错
- 代码可读性:明确告诉阅读者这是一个重写方法
- 防止笔误:如果写成
public String Work()(大写W),编译器会提示没有重写
深度原理:虚方法表(vtable)与动态绑定
什么是虚方法表
JVM 为每个类在方法区维护一张虚方法表(virtual method table),存储该类所有可被重写的实例方法的入口地址。
动态绑定流程
当 JVM 执行 emp.work() 时:
本质:虚方法调用(
invokevirtual指令)在运行时不依赖引用类型,而是通过对象头中的类型指针找到实际类的虚方法表,再查找具体方法入口。这就是"动态绑定"。
易错场景与避坑指南
反例一:重写时方法签名不一致
// ❌ 错误:参数类型不匹配,这不是重写,是重载!
public class Manager extends Employee {
@Override
public double computeAnnualSalary(int months) { // 参数多了 int months
return getSalary() * months + bonus;
}
}
// 编译错误:method does not override or implement a method from a supertype
小崔踩坑:不小心多写或少写参数,导致编译失败。@Override 注解帮你第一时间发现这类错误。
反例二:重写时访问权限缩小
public class Employee {
public String work() { return "工作中"; }
}
public class Manager extends Employee {
@Override
private String work() { // ❌ 编译错误:public → private 缩紧了权限
return "经理工作中";
}
}
纠正:子类方法的访问权限必须 ≥ 父类方法。public > protected > default > private。
反例三:构造方法中调用可重写方法
这是 Java 中一个经典陷阱:
// ❌ 危险:构造方法中调用了可被子类重写的方法
public class Employee {
public Employee() {
printInfo(); // 此时子类构造还未执行,子类字段为默认值
}
public void printInfo() {
System.out.println("Employee info");
}
}
public class Manager extends Employee {
private double bonus = 100000; // 字段初始化
public Manager() {
super(); // 调用父类构造 → 触发 printInfo()
}
@Override
public void printInfo() {
System.out.println("Manager bonus: " + bonus);
}
}
// 测试
public class Test {
public static void main(String[] args) {
Manager m = new Manager();
}
}
运行输出:
Manager bonus: 0.0 ← 不是 100000!因为 bonus 初始化还未执行
Manager bonus: 100000.0 ← 第二次调用(如果后续调用)才是正确的
原理:构造方法调用链为 Object() → Employee() → Manager()。当 Employee() 中调用 printInfo() 时,Manager() 还未执行,bonus 还是默认值 0.0。
纠正:构造方法中只调用 private、final 或 static 方法,这些方法不会被重写。
// ✅ 正确:构造方法只调用 private/final/static 方法
public class Employee {
public Employee() {
initInternal(); // private 方法,不会被重写
}
private void initInternal() {
System.out.println("Employee init");
}
}
面试考点
Q1:Java 为什么不支持多继承?
Java 设计者为了避免 C++ 多继承的"钻石问题"——当两个父类拥有同名方法时,编译器无法确定子类应该调用哪个。Java 通过"单继承 + 多接口实现"的方案提供了更清晰的设计。从 Java 8 开始,接口支持
default方法,但规定了严格的冲突解决规则(类优先于接口)。
Q2:方法重写(Override)和方法重载(Overload)有什么区别?
| 对比维度 | Override(重写) | Overload(重载) |
|---|---|---|
| 发生位置 | 父子类之间 | 同一个类中 |
| 方法签名 | 完全相同 | 方法名相同,参数列表不同 |
| 返回值 | 相同或协变 | 可以不同 |
| 访问权限 | 不能更严格 | 无限制 |
| 多态性 | 运行时多态 | 编译时多态 |
| 注解 | @Override | 无特定注解 |
Q3:@Override 注解是必需的吗?不加会怎样?
不是必需的,代码可以编译通过。但强烈建议添加,因为它提供编译期检查——如果方法签名写错(如参数类型不匹配),编译器会报错,避免把"重写"写成"重载"的隐蔽 bug。
Q4:构造方法中为什么不应该调用可重写方法?
因为构造方法链的执行顺序是从父类到子类。当父类构造方法执行时,子类的字段尚未初始化(为默认值),此时如果父类调用了一个被子类重写的方法,这个方法会读到子类字段的默认值而非预期值,导致难以调试的 bug。