继承(extends)
本教程共 100 篇 · 第 39 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
39. 继承(extends)
本节目标:学会用
extends让一个类白捡另一个类的全部功能,理解 Java 的单继承规则和 Object 根类,写代码时不再复制粘贴。
为什么需要继承
先看一段让人手痒的代码。我们已经有了一个 Person 类:
class Person {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
```java
现在要再写一个 `Student` 类。学生也有姓名、年龄,只是多一个分数:
```java
class Student {
private String name; // 和 Person 一模一样
private int age; // 和 Person 一模一样
private int score; // 新增的
// getName/setName/getAge/setAge 全部要再抄一遍……
}
抄一遍不难,难的是以后改。哪天 name 要加个长度校验,两个类都得改。
写三个类就抄三遍,写十个类就抄十遍。这种重复是 bug 的温床。
用 extends 复用代码
Java 用 extends 关键字表达「继承」:
class Person {
protected String name;
protected int age;
public String getName() { return name; }
public int getAge() { return age; }
}
// Student 继承 Person,自动拥有 name、age、getName()、getAge()
class Student extends Person {
private int score; // 只写新增的字段
public int getScore() { return score; } // 只写新增的方法
public void setScore(int score) { this.score = score; }
}
```bash
`Student` 里一个字都没提 `name`,但它确实有 `name`。继承让子类自动获得父类的全部字段和方法。
术语上,`Person` 叫**父类**(也叫超类、基类),`Student` 叫**子类**(也叫扩展类)。本教程统一用「父类 / 子类」。
> [!WARNING]
> 子类不要定义和父类重名的字段。虽然编译能过,但父类字段会被「遮蔽」,同一个名字在不同代码里指向不同的东西,排查起来很痛苦。
## 继承树与 Object 根类
`Person` 定义时并没有写 `extends`,那它继承自谁?
答案是 `Object`。编译器会自动补上 `extends Object`。所以 Java 里任何类都有父类,唯一的例外是 `Object` 自己——它站在继承树的最顶端。
```text
┌──────────┐
│ Object │
└──────────┘
▲
│
┌──────────┐
│ Person │
└──────────┘
▲ ▲
│ │
┌──────────┐ ┌──────────┐
│ Student │ │ Teacher │
└──────────┘ └──────────┘
这带来两个直接结论。第一,任何对象都能调用 toString()、equals()、hashCode(),因为这些方法定义在 Object 上。
第二,Object 类型的变量可以接住任何对象:
Object o1 = new Student();
Object o2 = "hello";
Object o3 = 123; // int 自动装箱成 Integer,Integer 也是 Object 的子孙
```bash
## Java 只允许单继承
有些语言允许一个类同时继承多个父类,Java 不行。
```java
class A {}
class B {}
// 编译错误:类不能继承多个父类
// class C extends A, B {}
一个类有且只有一个直接父类。这条规则听起来限制很大,但它避免了「菱形继承」的歧义:如果 A 和 B 都有 run() 方法,C.run() 该听谁的?
Java 的做法是:类只能单继承,但接口可以实现多个。多重能力的需求交给接口去解决,这个我们在第 45 章会详细讲。
Note单继承指的是「直接父类只有一个」。继承链可以很长:
PrimaryStudent extends Student extends Person extends Object,四层没问题。
protected:给子类开的后门
继承有个容易踩的坑:子类访问不到父类的 private 成员。
class Person {
private String name;
}
class Student extends Person {
public String hello() {
return "Hello, " + name; // 编译错误:name 是 private
}
}
```java
`private` 的意思是「只有本类内部可见」,子类也算外人。这时候把修饰符改成 `protected` 就通了:
```java
class Person {
protected String name; // 改成 protected
protected int age;
}
class Student extends Person {
public String hello() {
return "Hello, " + name; // 现在可以访问
}
}
protected 的作用范围是「本类 + 子类 + 同包」。它把访问权限控制在继承树内部,比 public 收敛,比 private 宽松。
实际项目里,字段更常见的做法仍是 private + protected 的 getter。这样父类能随时改内部实现,不怕子类直接摸字段。
子类构造方法必须先调父类
这是初学者最容易撞墙的地方。看这段代码:
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
this.score = score; // 编译错误!
}
}
```java
报错信息大意是「找不到 `Person()` 构造方法」。为什么会去找一个我们没写的构造方法?
因为 Java 规定:**任何构造方法的第一行,必须是调用父类的构造方法**。如果你没写,编译器会偷偷加一句 `super();`:
```java
public Student(String name, int age, int score) {
super(); // 编译器自动加的,调用 Person()
this.score = score;
}
而 Person 只有带参构造方法,没有无参的,所以找不到,编译失败。
解决办法是自己显式调用一个存在的父类构造方法:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 明确调用 Person(String, int)
this.score = score;
}
}
```bash
> [!TIP]
> 记住一条规律:**父类没有无参构造方法时,子类必须显式写 `super(...)`**。这也是很多人给类保留一个无参构造方法的原因。
顺带澄清一个概念:**构造方法不会被继承**。子类那个「默认构造方法」是编译器现生成的,不是从父类抄来的。
## 完整可运行示例
把上面的知识串起来跑一遍:
```java
public class Main {
public static void main(String[] args) {
Student s = new Student("小明", 12, 89);
System.out.println(s.getName()); // 小明 —— 来自 Person
System.out.println(s.getAge()); // 12 —— 来自 Person
System.out.println(s.getScore()); // 89 —— Student 自己的
System.out.println(s.hello()); // Hello, 小明
// 继承树验证
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Object); // true
}
}
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
public String hello() { return "Hello, " + name; }
}
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age);
this.score = score;
}
public int getScore() { return score; }
}
编译运行:
javac Main.java
java Main
```bash
## 继承是 is-a,组合是 has-a
最后说个设计问题,比语法更重要。
假设有个 `Book` 类,它也有 `name` 字段。能不能让 `Student extends Book` 来省事?
```java
// 语法没问题,逻辑荒唐
class Student extends Book {
protected int score;
}
编译器不会拦你,但这是错的。判断标准很简单:子类必须「是一种」父类。
- 学生是一种人 →
Student extends Person✅ - 学生是一种书?→ 显然不是 ❌
学生和书的关系是「拥有」——学生有一本书。这种关系要用组合,也就是把对象当字段持有:
class Student extends Person { // is-a:用继承
protected Book book; // has-a:用组合
protected int score;
}
```bash
> [!TIP]
> 拿不准该用继承还是组合时,先在心里念一句「A 是一种 B 吗」。念着别扭就别用继承。实际开发中组合的使用频率远高于继承。
## 小结
- `extends` 让子类自动获得父类的字段和方法,避免重复代码。
- Java 只允许单继承,一个类只有一个直接父类;所有类的最终祖先是 `Object`。
- `private` 成员子类访问不到,需要子类访问就用 `protected`。
- 子类构造方法第一行必须调父类构造方法,父类没有无参构造时必须显式写 `super(...)`。
- 构造方法不会被继承。
- 继承表达 is-a 关系,has-a 关系应该用组合。
下一章讲**方法重写**——子类如何把父类的方法改成自己的行为。