- 0
- 488 words
14. 继承进阶:super 关键字与构造器链
上一篇文章我们认识了继承的基本概念:用 extends 建立父子类关系,子类继承父类的成员,实现代码复用。但有个细节你可能已经注意到了——当我们创建子类对象时,父类的构造器似乎也被触发了?子类能不能调用父类的构造器?如果父子类有同名成员,子类怎么明确地访问父类的版本?
这一篇就来深入这些”进阶”问题,核心就是三个字——super。
一、super 是什么?
super 是 Java 提供的一个关键字,用来引用当前对象的直接父类部分。它有两个主要用途:
- 访问父类的成员(字段、方法)——当子类重写或隐藏了父类成员时
- 调用父类的构造器——在子类构造器中,通过
super(...)调用父类构造器
你可以把 super 理解成”父类版的 this“,它指明了”我要的是父类的那个版本”。
1. super 访问父类成员
当子类定义了与父类同名的字段或方法时,子类中的名字会遮蔽(hide)或重写(override)父类的版本。这时候如果想访问父类的版本,就要用 super。
class Animal {
String name = "动物";
void speak() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
String name = "小狗"; // 遮蔽了父类的 name
@Override
void speak() {
super.speak(); // 调用父类的 speak()
System.out.println("汪汪!");
}
void showName() {
System.out.println("子类 name:" + this.name); // "小狗"
System.out.println("父类 name:" + super.name); // "动物"
}
}
运行 showName() 会输出:
子类 name:小狗
父类 name:动物
注意:字段访问是由声明类型决定的,不是由实际类型决定的。但方法调用是动态绑定的-一别急,这个在下一篇《方
法重写》里会详细讲。
二、构造器链:子类对象创建时发生了什么?
这是继承中最重要的机制之一。Java 保证:创建任何子类对象时,一定会先调用父类的构造器,从最顶层的父类一直往下,最后才执行子类构造器的代码体。这条调用链条就叫构造器链(Constructor Chaining)。
1. 隐式调用:super()
如果你在子类构造器中没有写 super(...),编译器会自动插入一个无参的 super() 调用:
class Animal {
public Animal() {
System.out.println("Animal 构造器执行");
}
}
class Dog extends Animal {
public Dog() {
// 这里等价于:super(); // 编译器自动添加
System.out.println("Dog 构造器执行");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
}
}
输出结果:
Animal 构造器执行
Dog 构造器执行
看到没?先父后子。从继承的角度解释:子类对象的内存结构中,父类的成员变量是”基底”,必须先初始化好,子类的扩展部分才能在此基础上搭建。
2. 显式调用:super(参数)
如果父类没有无参构造器(只有一个有参构造器),那么子类构造器必须显式调用 super(参数),否则编译报错。
class Animal {
String name;
// 只有有参构造器,没有无参构造器
public Animal(String name) {
this.name = name;
System.out.println("Animal 构造器:" + name);
}
}
class Dog extends Animal {
public Dog(String name) {
super(name); // 必须显式调用,否则编译错误
System.out.println("Dog 构造器:" + name);
}
}
输出:
Animal 构造器:旺财
Dog 构造器:旺财
重要规则:superQ或super(参数)必须是子类构造器中的第一条语句。否则会报编译错误。这条规则保证了父类的初始
化一定在子类之前。
3. 完整的构造器链执行顺序
结合多层继承来看:
class GrandParent {
public GrandParent() {
System.out.println("1. GrandParent");
}
}
class Parent extends GrandParent {
public Parent() {
System.out.println("2. Parent");
}
}
class Child extends Parent {
public Child() {
System.out.println("3. Child");
}
}
public class Main {
public static void main(String[] args) {
new Child();
}
}
输出:
1. GrandParent
2. Parent
3. Child
执行流程就像叠罗汉:从最顶层到最底层,构造器链逐级向下执行。
三、this() 与 super() 的互斥
你可能还记得 this() 可以在同一个类中调用另一个构造器。但 this() 和 super() 不能同时出现在同一个构造器中——因为两者都必须是第一条语句,不可能同时满足。
不过,可以这样链式调用:
class Animal {
String name;
int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Dog extends Animal {
String breed;
public Dog(String name, int age, String breed) {
super(name, age); // 调用父类构造器
this.breed = breed;
}
public Dog(String name) {
this(name, 0, "未知"); // this() 调用本类另一个构造器
// this 内部最终会通过 super() 或 super(...) 调用父类构造器
}
}
上面的 Dog(String name) 先调用了 this(name, 0, "未知"),而那个三参构造器里又调用了 super(name, age)。最终父类构造器仍然最先被执行。
四、多层继承(继承链)
Java 不允许多继承(一个类 extends 多个父类),但支持多层继承——A → B → C → D……形成一个继承链。
class Vehicle {
void start() {
System.out.println("交通工具启动");
}
}
class Car extends Vehicle {
void drive() {
System.out.println("开车上路");
}
}
class SUV extends Car {
void offRoad() {
System.out.println("越野模式");
}
}
SUV 继承自 Car,Car 继承自 Vehicle。所以 SUV 对象可以:
- 调用自己的
offRoad() - 调用父类的
drive() - 调用祖父类的
start()
这就是继承的传递性。在实践中,继承链通常建议不超过 3~4 层,太深会增加维护成本和理解难度。
五、访问修饰符在继承中的影响
不同的访问修饰符决定了子类能访问多少父类的成员:
| 修饰符 | 本类 | 同包 | 子类(不同包) | 任意类 |
|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ |
| 默认(无修饰符) | ✅ | ✅ | ❌ | ❌ |
protected | ✅ | ✅ | ✅ | ❌ |
public | ✅ | ✅ | ✅ | ✅ |
protected 是继承场景中最特殊的一个:它给子类开了权限,但对外部仍然封闭。如果一个字段预期会被子类使用,就声明为 protected。
class Animal {
private int id; // 子类不可见
String type; // 同包子类可见,不同包子类不可见
protected int age; // 子类可见
public String name; // 全部可见
private void secret() {} // 子类不可调用
protected void eat() {} // 子类可调用
}
六、常见误区与最佳实践
❌ 误区 1:忘记写 super() 导致父类无参构造器不存在时报错
如果父类只定义了有参构造器,子类必须显式 super(...)。最佳实践:建议每个类都显式声明无参构造器(除非确实不需要通过无参方式创建)。
❌ 误区 2:在普通方法中使用 super()
super() 和 this() 只能用在构造器里。普通方法中访问父类成员直接用 super.成员名,不能加括号。
❌ 误区 3:滥用多层继承
继承链过长 → 耦合度过高 → 改底层影响一大片。能用组合就用组合,这个后面会专门讲。
七、代码实战:员工管理系统
来一个综合案例,把今天学的知识串起来。
// 父类
class Employee {
protected String name;
protected double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
System.out.println("Employee 构造器:" + name);
}
public void work() {
System.out.println(name + " 正在工作");
}
public double getSalary() {
return salary;
}
}
// 子类:经理
class Manager extends Employee {
private double bonus;
public Manager(String name, double salary, double bonus) {
super(name, salary); // 调用父类构造器
this.bonus = bonus;
System.out.println("Manager 构造器:" + name);
}
@Override
public void work() {
super.work(); // 调用父类的 work
System.out.println(name + " 在管理团队");
}
@Override
public double getSalary() {
return super.getSalary() + bonus; // 调用父类方法获取基础薪资再加奖金
}
}
// 子类:程序员
class Programmer extends Employee {
private String skill;
public Programmer(String name, double salary, String skill) {
super(name, salary);
this.skill = skill;
}
@Override
public void work() {
super.work();
System.out.println(name + " 在写 " + skill + " 代码");
}
}
public class Main {
public static void main(String[] args) {
Manager mgr = new Manager("张三", 20000, 5000);
System.out.println("---");
Programmer coder = new Programmer("李四", 15000, "Java");
System.out.println("n=== 工作 ===");
mgr.work();
System.out.println("---");
coder.work();
System.out.println("n=== 薪资 ===");
System.out.println(mgr.name + ":" + mgr.getSalary());
System.out.println(coder.name + ":" + coder.getSalary());
}
}
运行结果:
Employee 构造器:张三
Manager 构造器:张三
---
Employee 构造器:李四
=== 工作 ===
张三 正在工作
张三 在管理团队
---
李四 正在工作
李四 在写 Java 代码
=== 薪资 ===
张三:25000.0
李四:15000.0
这个案例清晰地展示了:
- 构造器链的执行顺序(先父后子)
super()调用父类构造器传参super.方法()复用父类逻辑protected让子类可以直接访问父类字段- 方法重写配合
super实现”扩展而非替换”
结语
这一篇我们从 super 关键字出发,深入了几重继承的关键机制——构造器链的执行顺序、super() 与 this() 的关系、多层继承、访问修饰符在继承中的作用。这些都是 Java 继承中最容易出错但又最重要的基础。
下一篇我们将进入一个更精彩的话题——方法重写(Override)与 final 关键字,这是面向对象多态的核心前提。到时候我们会深入 Java 的动态绑定机制,看看 “重写” 和 “重载” 到底有什么区别。