子类构造器与 this(...) / super(...) | JavaSE
子类构造器与 this(...) / super(...)
一、学习目标
学完本章,你应该能够:
- 能够解释创建子类对象时为什么会先执行父类构造器。
- 能够解释子类构造器中隐式
super()的含义。 - 能够判断父类没有无参数构造器时为什么子类可能编译失败。
- 能够使用
super(...)调用父类指定构造器。 - 能够使用
this(...)调用当前类中的其他构造器。 - 能够区分
this(...)、super(...)、this.xxx、super.xxx。 - 能够解释 JDK 21 中为什么
this(...)和super(...)必须位于构造器第一条语句。 - 能够理解构造器链(Constructor Chaining)。
- 能够设计
People → Teacher等继承体系中的对象初始化流程。 - 能够 Debug 常见的父类构造器不存在、构造器递归调用等问题。
二、核心知识
2.1 先回顾:构造器不能被继承
前面已经学习:
public class People {
public People() {
System.out.println("People()");
}
}
子类:
public class Teacher extends People {
}
虽然:
Teacher
继承了 People 的部分成员能力,但是:
构造器不会被子类继承。
所以:
People()
不会自动变成:
Teacher()
这两个构造器分别属于两个不同的类。
但是马上会出现一个问题:
如果构造器不能继承,创建
Teacher时为什么还会执行People的构造器?
这就是本章真正要解决的问题。
2.2 创建子类对象时为什么要初始化父类部分
假设:
public class People {
private String name;
private char sex;
}
子类:
public class Teacher extends People {
private String skill;
}
一个 Teacher 对象从类型模型上不仅拥有:
Teacher 自己定义的部分
skill
还包含继承体系中由 People 定义的状态:
People 部分
name
sex
所以创建:
Teacher teacher = new Teacher();
不能只初始化:
skill
而完全不处理它的父类部分。
可以先建立这样一个概念模型:
Teacher 对象
┌──────────────────────────┐
│ People 定义的对象状态 │
│ name │
│ sex │
├──────────────────────────┤
│ Teacher 定义的对象状态 │
│ skill │
└──────────────────────────┘
因此在执行子类自己的初始化逻辑之前,需要先完成直接父类构造过程。
这就是为什么创建子类对象时会发生:
父类构造过程
↓
子类构造过程
2.3 最简单的构造器执行顺序
父类:
public class Fu {
public Fu() {
System.out.println("父类构造器执行");
}
}
子类:
public class Zi extends Fu {
public Zi() {
System.out.println("子类构造器执行");
}
}
测试:
public class Test {
public static void main(String[] args) {
Zi zi = new Zi();
}
}
执行顺序是:
父类构造器执行
子类构造器执行
为什么?
因为子类构造器实际上存在一个重要调用:
public Zi() {
super();
System.out.println("子类构造器执行");
}
只不过:
super();
通常被编译器隐式补充,所以我们没有手写出来。
2.4 super() 是什么
在构造器中:
super();
表示:
调用当前类直接父类的无参数构造器。
例如:
class Animal {
public Animal() {
System.out.println("Animal()");
}
}
class Dog extends Animal {
public Dog() {
super();
System.out.println("Dog()");
}
}
这里:
super();
调用的是:
Animal()
不是任意更上层类的构造器。
也就是说:
Dog
↓ super()
Animal
如果 Animal 自己还继承其他类,则由 Animal 的构造器继续完成它自己的父类构造过程。
2.5 默认情况下存在隐式 super()
JDK 21 中,如果某个构造器:
- 不是
Object构造器; - 开头没有显式调用当前类其他构造器;
- 也没有显式调用直接父类构造器;
那么编译器会按照语言规则把它理解为首先调用:
super();
例如:
public class Dog extends Animal {
public Dog() {
System.out.println("Dog()");
}
}
可以理解成:
public Dog() {
super();
System.out.println("Dog()");
}
因此一定要记住:
“我没有写 super()”不代表父类构造器不会被调用。
2.6 父类必须有能够匹配的无参构造器吗
观察父类:
public class People {
public People(String name) {
System.out.println(name);
}
}
注意:
People
已经显式声明了:
People(String name)
因此编译器不会再为它自动提供默认无参构造器:
People()
此时子类:
public class Teacher extends People {
public Teacher() {
}
}
会发生什么?
子类构造器没有显式写:
super(...)
因此按照规则会尝试:
super();
也就是寻找:
People()
但父类并不存在这个构造器。
于是:
编译失败。
2.7 父类没有无参构造器怎么办
假设:
public class People {
private String name;
public People(String name) {
this.name = name;
}
}
子类必须选择一个实际存在的父类构造器:
public class Teacher extends People {
private String skill;
public Teacher(String name, String skill) {
super(name);
this.skill = skill;
}
}
其中:
super(name);
会调用:
People(String name)
于是:
new Teacher("张老师", "Java")
│
↓
Teacher(String,String)
│
├── super(name)
│ ↓
│ People(String)
│ ↓
│ 初始化 name
│
└── 初始化 skill
这就是 super(...) 非常典型的实际应用。
2.8 super(...) 的核心作用
基本语法:
super(参数列表);
作用:
在子类构造器中调用直接父类中参数匹配的构造器。
例如父类:
public People(String name, char sex) {
this.name = name;
this.sex = sex;
}
子类:
public Teacher(String name, char sex, String skill) {
super(name, sex);
this.skill = skill;
}
这实际上形成了非常清晰的职责划分:
People 构造器
负责初始化 People 自己定义的状态
Teacher 构造器
负责初始化 Teacher 自己增加的状态
而不是让子类绕开父类封装,直接操作父类内部字段。
2.9 完整 People → Teacher 案例
父类:
public class People {
private String name;
private char sex;
public People() {
}
public People(String name, char sex) {
this.name = name;
this.sex = sex;
}
public String getName() {
return name;
}
public char getSex() {
return sex;
}
}
子类:
public class Teacher extends People {
private String skill;
public Teacher() {
}
public Teacher(String name, char sex, String skill) {
super(name, sex);
this.skill = skill;
}
public String getSkill() {
return skill;
}
}
测试:
public class Test {
public static void main(String[] args) {
Teacher teacher =
new Teacher(
"张老师",
'男',
"Java"
);
System.out.println(teacher.getName());
System.out.println(teacher.getSex());
System.out.println(teacher.getSkill());
}
}
这里:
super(name, sex);
负责把:
name
sex
交给:
People
自己的构造器初始化。
随后:
this.skill = skill;
初始化:
Teacher
新增状态。
2.10 this(...) 又是什么
前面学习:
this.name
表示当前对象中的成员。
但是在构造器中还存在一种特殊语法:
this(...)
例如:
public class Student {
private String name;
private String schoolName;
public Student(String name) {
this(name, "山西大学");
}
public Student(
String name,
String schoolName
) {
this.name = name;
this.schoolName = schoolName;
}
}
其中:
this(name, "山西大学");
不是:
this.name
也不是普通方法调用。
它表示:
调用当前类中的另一个构造器。
2.11 this(...) 解决什么问题
假设需要两个创建方式:
new Student("张三");
以及:
new Student("李四", "山西大学");
业务规则规定:
如果没有指定学校,则默认学校是“山西大学”。
可以直接:
public Student(String name) {
this.name = name;
this.schoolName = "山西大学";
}
另一个构造器:
public Student(String name, String schoolName) {
this.name = name;
this.schoolName = schoolName;
}
但是:
this.name = name;
出现了重复。
可以改成:
public Student(String name) {
this(name, "山西大学");
}
于是:
Student(String name)
↓
this(name, "山西大学")
↓
Student(String name, String schoolName)
↓
统一完成初始化
这样可以:
让多个构造器复用同一个初始化入口。
2.12 this(...) 与 super(...) 的区别
这是本章必须闭卷掌握的表格:
| 语法 | 调用对象 | 作用 |
| ------------ | ---------------- | -------------- |
| this(...) | 当前类其他构造器 | 构造器之间复用 |
| super(...) | 直接父类构造器 | 初始化父类部分 |
例如:
this(name, "山西大学");
寻找的是:
当前类
Student(...)
而:
super(name, sex);
寻找的是:
直接父类
People(...)
千万不要混淆。
2.13 this(...) 和 this.xxx 完全不同
this.xxx
this.name = name;
这里:
this
表示当前对象。
this(...)
this(name, "山西大学");
这里是:
当前类其他构造器的显式调用。
因此:
this.name
和:
this(...)
虽然都使用 this,但语法用途完全不同。
2.14 super.xxx 和 super(...) 也不同
前一章学习:
super.run();
表示:
调用父类版本的成员方法。
例如:
super.name
表示父类视角成员。
而本章:
super(name);
表示:
调用直接父类构造器。
对比:
super.name
super.run()
↓
父类成员访问
super(...)
↓
父类构造器调用
2.15 JDK 21:this(...) 和 super(...) 必须放在哪里
在本课程的 JDK 21 基线中:
显式构造器调用必须位于构造器主体的第一条语句。
例如:
public Teacher(String name) {
super(name);
System.out.println("Teacher");
}
合法。
但:
public Teacher(String name) {
System.out.println("Teacher");
super(name);
}
在 JDK 21 中不能编译。
同样:
public Student(String name) {
this(name, "山西大学");
}
必须把:
this(...)
放在第一条语句。
2.16 为什么 this(...) 与 super(...) 不能同时写
错误:
public Student(String name) {
this(name, "山西大学");
super();
}
同样错误:
public Student(String name) {
super();
this(name, "山西大学");
}
因为在 JDK 21 中:
this(...)
和:
super(...)
都要求作为显式构造器调用出现在构造器第一条语句。
第一条语句只能有一条。
因此同一个构造器不能同时显式写:
this(...)
+
super(...)
2.17 使用 this(...) 后父类构造器还会执行吗
会。
这是非常重要的理解。
例如:
class Parent {
public Parent() {
System.out.println("Parent");
}
}
class Child extends Parent {
public Child() {
this(10);
System.out.println("Child()");
}
public Child(int number) {
System.out.println("Child(int)");
}
}
第二个构造器:
Child(int number)
没有显式写 this(...) 或 super(...)。
因此实际上会先执行:
super();
所以调用:
new Child();
构造器链可以理解为:
Child()
↓ this(10)
Child(int)
↓ 隐式 super()
Parent()
↓
返回 Child(int)
↓
返回 Child()
因此:
this(...)并不会让父类构造过程消失。
它只是把“调用父类构造器”的责任沿着当前类构造器链传递下去。
2.18 构造器链是什么
构造器链(Constructor Chaining)可以理解为:
创建一个对象时,通过
this(...)和super(...)形成的一系列构造器调用关系。
例如:
Object()
↑
People()
↑
Teacher(String,char,String)
↑
Teacher()
或者:
new Teacher()
↓
Teacher()
↓ this(...)
Teacher(String,char,String)
↓ super(...)
People(String,char)
↓ 隐式 super()
Object()
实际执行时,最上层父类构造逻辑先完成,再逐层返回执行子类构造器剩余代码。
所以经常可以观察到:
Object
↓
父类
↓
子类
这样的构造顺序。
2.19 构造器链最终到哪里
Java 普通类最终位于:
Object
继承体系中。
因此一个完整子类构造过程最终会追溯到:
Object()
例如:
Object
↑
Animal
↑
Dog
↑
Husky
创建:
new Husky();
最终会形成类似:
Husky(...)
↓
Dog(...)
↓
Animal(...)
↓
Object()
的构造链。
2.20 不能使用 this(...) 形成递归调用
观察:
public class Student {
public Student() {
this(10);
}
public Student(int age) {
this();
}
}
形成:
Student()
↓
Student(int)
↓
Student()
↓
Student(int)
↓
...
这不是普通运行时无限递归。
Java 编译器会检测这种构造器直接或间接调用自己的情况,并报告:
编译错误。
构造器调用链必须最终能够到达某个有效的:
super(...)
路径,而不能形成环。
三、使用方法
3.1 子类复用父类构造器初始化公共状态
父类:
public class Employee {
private String id;
private String name;
private double salary;
public Employee(
String id,
String name,
double salary
) {
this.id = id;
this.name = name;
this.salary = salary;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
}
子类:
public class Programmer extends Employee {
private String language;
public Programmer(
String id,
String name,
double salary,
String language
) {
super(id, name, salary);
this.language = language;
}
public String getLanguage() {
return language;
}
}
这里:
super(id, name, salary);
非常重要。
它明确告诉父类:
你定义的状态由你自己的构造器负责初始化。
3.2 使用 this(...) 提供默认值
public class User {
private String username;
private String role;
public User(String username) {
this(username, "USER");
}
public User(
String username,
String role
) {
this.username = username;
this.role = role;
}
}
使用:
User u1 = new User("LingXi");
User u2 =
new User(
"Admin",
"ADMIN"
);
这里:
u1
自动使用默认:
USER
而:
u2
可以显式指定角色。
3.3 同时存在 this(...) 与 super(...) 的正确结构
虽然同一个构造器不能同时显式写两者,但不同构造器之间可以形成:
public class Teacher extends People {
private String skill;
public Teacher(String name) {
this(name, '男', "Java");
}
public Teacher(
String name,
char sex,
String skill
) {
super(name, sex);
this.skill = skill;
}
}
构造链:
Teacher(String)
↓
this(...)
↓
Teacher(String,char,String)
↓
super(...)
↓
People(String,char)
这是非常经典的组合。
四、原理与进阶
4.1 “子类构造器先调用父类构造器”要准确理解
原课程常用一句话:
子类的全部构造器都会先调用父类构造器,再执行自己。
作为入门理解基本正确。
但更准确地说:
每一个子类构造过程最终都必须通过构造器链调用直接父类构造器;某个具体构造器可能先显式
this(...)调用本类另一个构造器,而不是自己直接写super(...)。
例如:
public Teacher(String name) {
this(name, "Java");
}
这个构造器首先直接调用的是:
Teacher(String,String)
不是:
People(...)
但后者最终还必须调用父类构造器。
4.2 为什么父类构造器不能被继承
构造器本质上负责:
初始化声明它的类对应的对象构造过程。
父类构造器名称:
People(...)
子类构造器名称:
Teacher(...)
二者属于不同类。
因此 Java 采用:
super(...)
明确让子类构造过程调用父类构造过程,而不是把父类构造器本身“继承成子类构造器”。
4.3 super(...) 不是创建一个父类对象
这是重要误区。
执行:
super(name, sex);
并不是:
new People(name, sex);
它不是额外创建一个独立 People 对象。
它是在:
当前正在创建的子类对象的构造过程中执行直接父类构造器。
所以:
Teacher teacher = new Teacher(...);
最终仍然只是在创建这个:
Teacher 对象
不是:
一个 People 对象
+
一个 Teacher 对象
拼起来。
4.4 JDK 21 与更新 Java 版本要区分
本教程严格以:
JDK 21
为教学基线。
所以必须掌握:
显式 this(...) / super(...)
必须位于构造器第一条语句
后续 Java 版本曾探索“Flexible Constructor Bodies”等语言变化,但不属于 JDK 21 主线规则。
因此当前学习、考试和本教程代码全部按照:
JDK 21 规则
处理。
五、实践应用
5.1 异常类以后也会大量看到 super(...)
未来学习自定义异常时,会看到:
public class AgeIllegalException
extends Exception {
public AgeIllegalException() {
}
public AgeIllegalException(String message) {
super(message);
}
}
为什么调用:
super(message);
因为异常信息:
message
属于父类异常体系已经定义好的状态。
子类可以直接复用父类构造器完成初始化。
所以本章的:
super(...)
不是孤立语法。
以后在:
- 自定义异常;
- GUI 类继承;
- 框架基类;
- Java 类库扩展;
中都会反复出现。
5.2 继承体系的初始化职责
良好的设计通常倾向于:
父类构造器
负责父类自己的状态
子类构造器
通过 super(...) 初始化父类状态
再初始化子类自己的状态
例如:
Card
├── carId
├── owner
└── balance
↑
GoldCard
└── discount
那么:
public GoldCard(
String carId,
String owner,
double balance,
double discount
) {
super(carId, owner, balance);
this.discount = discount;
}
逻辑就非常清晰。
六、常见问题
6.1 子类构造器会继承父类构造器吗?
不会。
构造器不能被继承。
6.2 为什么创建子类时父类构造器会执行?
因为子类对象包含继承体系中的父类状态,需要按照 Java 构造规则先完成父类构造过程。
6.3 每个子类构造器都必须手写 super() 吗?
不用。
如果没有显式 this(...) 或 super(...),JDK 21 会隐式按照:
super();
处理。
6.4 父类没有无参构造器怎么办?
子类必须选择一个真实存在、可访问且参数匹配的父类构造器:
super(...);
或者通过:
this(...)
调用当前类另一个最终能够正确调用父类构造器的构造器。
6.5 super(...) 调用祖父类构造器吗?
普通:
super(...)
直接调用:
当前类的直接父类构造器。
父类再负责继续调用它自己的父类。
6.6 this(...) 调用谁?
调用:
当前类中的另一个构造器。
6.7 this(...) 与 this.name 一样吗?
不一样。
this.name
访问当前对象成员。
this(...)
调用当前类另一个构造器。
6.8 super(...) 与 super.method() 一样吗?
不一样。
super(...)
调用父类构造器。
super.method()
调用父类成员方法实现。
6.9 this(...) 和 super(...) 为什么不能同时写?
JDK 21 要求显式构造器调用位于第一条语句。
同一个构造器不可能同时存在两个第一条语句。
6.10 this(...) 后父类构造器还执行吗?
执行。
当前类构造器链最终必须继续到父类构造过程。
6.11 this(...) 可以互相无限调用吗?
不能。
直接或间接形成构造器调用环会产生编译错误。
6.12 super(...) 会额外创建一个父类对象吗?
不会。
它是当前子类对象构造过程的一部分。
七、练习与验收
7.1 知识问答
- 构造器能否被继承?
- 创建子类对象时为什么需要执行父类构造器?
- 子类构造器默认第一步会尝试什么?
super()表示什么?super(参数)表示什么?- 父类只有有参构造器时,子类为什么可能报错?
- 如何解决父类没有无参构造器的问题?
this(...)表示什么?this(...)和this.xxx有什么区别?super(...)和super.xxx有什么区别?- JDK 21 中显式构造器调用必须放在哪里?
- 为什么同一个构造器不能同时显式写
this(...)与super(...)? - 使用
this(...)后父类构造器还会不会执行? - 什么是构造器链?
- Java 构造器链最终通常会到达哪个根类?
- 为什么
this(...)不能形成循环? super(...)是否意味着创建了一个独立父类对象?- 为什么父类自己的字段通常适合由父类构造器初始化?
7.2 代码阅读
不要运行:
class A {
public A() {
System.out.println("A");
}
}
class B extends A {
public B() {
System.out.println("B");
}
}
class C extends B {
public C() {
System.out.println("C");
}
}
public class Test {
public static void main(String[] args) {
new C();
}
}
回答:
- 三个构造器按照什么顺序执行?
B()中隐式存在什么?C()中隐式存在什么?- 完整构造器链是什么?
继续分析:
class Parent {
public Parent(int number) {
}
}
class Child extends Parent {
public Child() {
}
}
回答:
- 是否可以编译?
Child()隐式尝试调用哪个构造器?- 父类是否存在该构造器?
- 应该如何修复?
再阅读:
class Student {
public Student() {
this(18);
System.out.println("A");
}
public Student(int age) {
System.out.println("B");
}
}
回答:
new Student()先进入哪个构造器?this(18)调用哪个构造器?- 哪个构造器最终负责调用
super()? "A"和"B"谁先输出?
7.3 手写代码
任务一:People / Teacher
设计:
People
├── name
└── sex
Teacher extends People
└── skill
要求:
People提供无参构造器。People提供完整有参构造器。Teacher提供完整有参构造器。Teacher必须通过super(...)初始化父类状态。- 不允许为了方便把父类字段改成 public。
任务二:默认学校
设计:
Student
├── name
└── schoolName
要求:
new Student("张三");
默认学校:
山西大学
并允许:
new Student("李四", "北京大学");
必须使用:
this(...)
减少重复初始化代码。
任务三:三层构造器链
设计:
Person
↑
Employee
↑
Programmer
为每个类设计构造器,并在构造器中打印:
Person constructor
Employee constructor
Programmer constructor
创建 Programmer,预测执行顺序后再运行验证。
7.4 Debug
下面代码有什么问题?
class Parent {
public Parent(String name) {
}
}
class Child extends Parent {
public Child() {
System.out.println("Child");
}
}
要求:
- 不运行,解释编译失败原因。
- 找出编译器隐式尝试的调用。
- 给出合理修复方案。
下面代码有什么问题?
public Student(String name) {
System.out.println(name);
this(name, "山西大学");
}
要求:
- 是否符合 JDK 21?
- 哪条规则被违反?
- 修复代码。
下面代码:
public Student() {
this(18);
}
public Student(int age) {
this();
}
要求:
- 找出调用链。
- 为什么不能编译?
- 如何重新设计构造器关系?
7.5 综合训练
设计一个:
Card
├── GoldCard
└── SilverCard
Card 包含:
cardId
owner
balance
两个子类分别增加自己的折扣信息。
要求:
- 父类负责公共状态构造。
- 子类使用
super(...)初始化父类状态。 - 每个子类至少提供两个构造器。
- 其中一个构造器使用
this(...)调用另一个构造器。 - 画出完整构造器调用链。
- 说明每一层构造器分别负责初始化什么。
7.6 本章验收
关闭资料与代码提示,确认自己能够:
- [ ] 解释为什么创建子类先完成父类构造。
- [ ] 手写
super()。 - [ ] 手写
super(...)。 - [ ] 手写
this(...)。 - [ ] 区分
this.xxx与this(...)。 - [ ] 区分
super.xxx与super(...)。 - [ ] 判断父类没有无参构造器造成的编译错误。
- [ ] 解释 JDK 21 中第一条语句规则。
- [ ] 解释为什么
this(...)与super(...)不能同时显式出现。 - [ ] 画出三层构造器链。
- [ ] 识别构造器循环调用。
- [ ] 从零完成 People → Teacher 的构造初始化。
如果你看到:
super(...)
只知道它“和继承有关”,却无法画出完整构造器链,那么本章还没有真正掌握。