首页 / Java 入门教程 / super 关键字

Java 入门教程

super 关键字

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

JavaJava 入门教程super构造方法继承初始化顺序

41. super 关键字

本节目标:掌握 super 的三种用法,并搞清楚 new 一个子类对象时,父类和子类的代码到底按什么顺序执行。

super 指的是什么

this 指当前对象自己,super 指当前对象「作为父类的那一部分」。

注意措辞:super 不是另一个对象。一个 Student 实例只有一个对象,super 只是让你换个视角去访问同一个对象里属于父类的成员。

super 一共三种用法:

  1. super(...) —— 调用父类构造方法
  2. super.字段名 —— 访问父类字段
  3. super.方法名() —— 调用父类方法

下面逐个拆开。

用法一:super(…) 调用父类构造方法

这是最常用、也最容易出错的一种。

上一章说过,任何构造方法的第一行都必须调用父类构造方法。不写的话,编译器自动补 super();

class Person {
    protected String name;
    protected int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("Person 构造方法执行");
    }
}

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(name, age);          // 必须是第一行
        this.score = score;
        System.out.println("Student 构造方法执行");
    }
}
```java

`new Student("小明", 12, 89)` 的输出顺序是:

```text
Person 构造方法执行
Student 构造方法执行

先父后子。因为子类的字段初始化可能依赖父类的状态,父类不先准备好,子类就是在沙子上盖楼。

两条硬性规定

规定一:super(...) 必须是构造方法的第一条语句。

public Student(String name, int age, int score) {
    this.score = score;
    super(name, age);   // 编译错误:call to super must be first statement
}
```java

**规定二:`super(...)` 和 `this(...)` 不能同时出现。**

`this(...)` 是调用本类的另一个构造方法,它也要求在第一行。两个都想在第一行,只能二选一:

```java
class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(name, age);
        this.score = score;
    }

    // 简化版:委托给上面那个构造方法
    public Student(String name) {
        this(name, 0, 0);   // 调本类构造方法,它内部会调 super
    }
}

链条最终一定会走到某个 super(...),父类构造方法一次都不会漏。

Warning

父类如果没有无参构造方法,子类的每个构造方法都必须显式写 super(...)。这是初学者最常见的编译错误之一,报错信息通常是 constructor Xxx in class Xxx cannot be applied to given types

用法二:super.字段 访问父类字段

正常情况下,子类访问父类字段直接写字段名就行,编译器会自动往上找:

class Student extends Person {
    public String hello() {
        return "Hello, " + name;         // 直接写
        // return "Hello, " + this.name; // 等价
        // return "Hello, " + super.name;// 也等价
    }
}
```java

三种写法效果一样。那 `super.name` 什么时候才是**必需**的?

答案是:**父子类有同名字段时**

```java
class Person {
    protected String name = "父类的名字";
}

class Student extends Person {
    protected String name = "子类的名字";   // 遮蔽了父类的 name

    public void print() {
        System.out.println(name);        // 子类的名字
        System.out.println(this.name);   // 子类的名字
        System.out.println(super.name);  // 父类的名字
    }
}

这种现象叫字段遮蔽(field hiding)。父类那个 name 并没有消失,仍然占着内存,只是被子类的同名字段挡住了,必须用 super. 才能摸到。

Warning

字段遮蔽在实际开发中几乎全是 bug 来源,不要主动使用。第 39 章说的「严禁定义与父类重名的字段」就是这个意思。这里讲它,是为了让你在读别人代码时看得懂。

用法三:super.方法() 调用父类方法

上一章已经见过,这里补充两个要点。

要点一:这是「扩展」而非「替换」的标准写法。

class Person {
    public String describe() {
        return "姓名=" + name + ", 年龄=" + age;
    }
}

class Student extends Person {
    @Override
    public String describe() {
        return super.describe() + ", 分数=" + score;   // 父类的信息 + 自己的
    }
}
```java

重写 `toString()` 时这招特别顺手,不用把父类字段再拼一遍。

**要点二:`super.方法()` 是唯一能「穿透多态」的写法。**

普通调用 `this.describe()` 会走多态,执行最下层子类的版本。只有 `super.describe()` 明确锁定父类那一份,不受多态影响。

```java
class Person {
    public void run() { System.out.println("Person.run"); }
}

class Student extends Person {
    @Override
    public void run() { System.out.println("Student.run"); }

    public void test() {
        run();         // Student.run —— 走多态
        this.run();    // Student.run —— 同上
        super.run();   // Person.run  —— 锁定父类
    }
}
Note

super 只能往上跳一层。没有 super.super.run() 这种写法,Java 不支持越级调用祖父类的方法。

对象初始化的完整顺序

super(...)、字段初始化、代码块放一起,new 一个子类对象时的执行顺序是这样的:

  1. 父类的静态字段和静态代码块(只在类首次加载时执行一次)
  2. 子类的静态字段和静态代码块(同上,只执行一次)
  3. 父类的实例字段初始化和实例代码块
  4. 父类的构造方法体
  5. 子类的实例字段初始化和实例代码块
  6. 子类的构造方法体

用代码验证:

public class Main {
    public static void main(String[] args) {
        System.out.println("--- 第一次 new ---");
        new Student();
        System.out.println("--- 第二次 new ---");
        new Student();
    }
}

class Person {
    static { System.out.println("1. Person 静态代码块"); }
    { System.out.println("3. Person 实例代码块"); }

    public Person() { System.out.println("4. Person 构造方法"); }
}

class Student extends Person {
    static { System.out.println("2. Student 静态代码块"); }
    { System.out.println("5. Student 实例代码块"); }

    public Student() { System.out.println("6. Student 构造方法"); }
}
```bash

运行:

```bash
javac Main.java
java Main

输出:

--- 第一次 new ---
1. Person 静态代码块
2. Student 静态代码块
3. Person 实例代码块
4. Person 构造方法
5. Student 实例代码块
6. Student 构造方法
--- 第二次 new ---
3. Person 实例代码块
4. Person 构造方法
5. Student 实例代码块
6. Student 构造方法
```bash

第二次 new 时静态块不再执行——它们只在类加载时跑一次。

## 构造方法里调用可重写方法:一个经典陷阱

这个坑值得单独拎出来。

```java
class Person {
    public Person() {
        init();          // 危险:调用了可被重写的方法
    }

    public void init() {
        System.out.println("Person.init");
    }
}

class Student extends Person {
    private String name = "小明";

    @Override
    public void init() {
        System.out.println("Student.init, name = " + name);
    }
}

new Student() 输出的是:

Student.init, name = null
```bash

`name` 明明赋了值,怎么是 `null`?

回顾初始化顺序:父类构造方法(第 4 步)比子类字段初始化(第 5 步)更早。父类构造方法里调 `init()`,多态让它跳到 `Student.init()`,而这时 `name` 还没来得及赋值,只有默认值 `null`。

> [!WARNING]
> **不要在构造方法里调用可被重写的方法**。要么把方法声明为 `final`/`private`,要么把初始化逻辑挪到构造方法之外。这是《Effective Java》明确点名的反模式,也是面试常考点。

## super 与 this 对照

| 对比项 | this | super |
|--------|------|-------|
| 指代 | 当前对象自身 | 当前对象的父类部分 |
| 调构造方法 | `this(...)` 调本类其他构造方法 | `super(...)` 调父类构造方法 |
| 访问字段 | `this.x` 优先找本类 | `super.x` 直接找父类 |
| 调方法 | `this.m()` 走多态 | `super.m()` 锁定父类实现 |
| 静态方法中能否用 | 不能 | 不能 |

两者都不能出现在 `static` 方法里,因为静态方法属于类,压根没有「当前对象」。

## 小结

- `super` 代表当前对象的父类部分,不是另一个对象。
- `super(...)` 调父类构造方法,必须在第一行,且不能与 `this(...)` 共存。
- `super.字段` 用于突破字段遮蔽;日常不要制造同名字段。
- `super.方法()` 用于在重写中复用父类实现,且不受多态影响,只能上跳一层。
- 初始化顺序:父静态 → 子静态 → 父实例块 → 父构造 → 子实例块 → 子构造。
- 别在构造方法里调用可重写方法,否则会读到未初始化的子类字段。

下一章讲 `final`,看看怎么主动关上继承和重写这两扇门。