super 关键字
本教程共 100 篇 · 第 41 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
41. super 关键字
本节目标:掌握
super的三种用法,并搞清楚 new 一个子类对象时,父类和子类的代码到底按什么顺序执行。
super 指的是什么
this 指当前对象自己,super 指当前对象「作为父类的那一部分」。
注意措辞:super 不是另一个对象。一个 Student 实例只有一个对象,super 只是让你换个视角去访问同一个对象里属于父类的成员。
super 一共三种用法:
super(...)—— 调用父类构造方法super.字段名—— 访问父类字段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 一个子类对象时的执行顺序是这样的:
- 父类的静态字段和静态代码块(只在类首次加载时执行一次)
- 子类的静态字段和静态代码块(同上,只执行一次)
- 父类的实例字段初始化和实例代码块
- 父类的构造方法体
- 子类的实例字段初始化和实例代码块
- 子类的构造方法体
用代码验证:
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`,看看怎么主动关上继承和重写这两扇门。