首页 / Java 入门教程 / 继承(extends)

Java 入门教程

继承(extends)

本教程共 100 篇 · 第 39 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程继承extendsObject面向对象

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 关系应该用组合。

下一章讲**方法重写**——子类如何把父类的方法改成自己的行为。