写 Java 的人迟早要和 super 打交道尤其是刚把继承、重写、多态这几个概念混在一起的时候。很多人第一反应是把它当成和 this 一样的东西——this 指自己super 指父类两个都是引用嘛。这个理解大方向没错但细节上差得很远。等你真在代码里写super.xxx的时候问题就来了为什么 super() 必须放在构造器第一行为什么父类没有无参构造器子类就编译不过为什么 super 不能出现在静态方法里还有最阴险的一道题在子类里调用 super 的方法真的能完全绕过多态吗这篇文章我打算把 super 彻底拆开讲一遍从内存视角讲清楚它到底是什么再逐一过它的三大核心用法、构造器调用链的完整规则最后用实际案例演示多层继承里 super 的流转过程顺带把面试里最常考的几种变形题一并说透。1. super 到底是什么从 this 的对比说起1.1 先纠正一个误解super 不是引用变量很多教材把 this 和 super 并列讲解给人的暗示是它们都是某种引用一个指向当前对象一个指向父类对象。这句话在“帮助理解”的层面勉强能接受但如果你真的把它当作事实去推导就会得出一个错误结论——以为 super 可以像 this 一样被赋值、被传递、被打印。实际上 super 根本不是一个独立的引用变量。你试着写下面这段代码编译器会直接报错public class Demo { public void test() { Object obj super; // 编译报错意外的类型 System.out.println(super); // 编译报错 } }为什么因为 this 是实实在在保存在局部变量表里的引用底层字节码是一条aload_0它可以作为参数传给别的方法。而 super 是编译器层面的“语法糖”它本身没有对应的存储位置。编译器看到super.xxx时会把它翻译成一条特殊的字节码指令比如getfield、invokespecial指令的作用目标是“当前对象里属于父类的那一部分内存”而不是另一个独立的对象。理解这一点特别重要super 不是从对象外部指向父类对象而是从对象内部定位到父类成员。1.2 内存视角子类对象里根本没有两个对象再来回答一个经典疑问new 一个子类对象堆上是不是创建了两个对象——一个父类对象一个子类对象不是。JVM 在new一个子类时只分配一块连续的内存这块内存里同时包含了父类声明的所有字段和子类自己声明的字段。你可以把子类对象想象成一栋带地基的房子地基是父类部分上面盖的楼层是子类部分整栋房子是一体的。super 这个语法的作用不是带你去隔壁看另一栋房子而是让你把目光聚焦在当前这栋房子的地基区域。所以“父类对象”这个概念在子类实例场景下是不存在的。存在的只是一个对象实例内部被划分成两个逻辑区域。这也是为什么 super 不能脱离 this 单独存在——它不是独立引用而是依附在当前对象上的一种访问方式。到这里最基本的概念就清楚了。接下来看 super 在代码里的三个典型用法。2. super 的三大核心用法逐个拆解2.1 用 super 访问父类的成员变量和方法当子类定义了和父类同名的字段或者重写了父类的方法子类内部直接写这个成员名时默认访问到的是子类的版本。如果你想强行访问父类那份就需要 super 出场。class Animal { String name 动物; public void eat() { System.out.println(动物在吃东西); } } class Dog extends Animal { String name 狗; public void show() { System.out.println(父类里的 name super.name); System.out.println(子类里的 name this.name); System.out.println(直接写 name name); // 等价于 this.name } Override public void eat() { System.out.println(狗在啃骨头); } public void callSuperEat() { super.eat(); // 手动调用父类被重写的方法 } } public class Demo { public static void main(String[] args) { Dog dog new Dog(); dog.show(); dog.eat(); dog.callSuperEat(); } }运行结果父类里的 name动物 子类里的 name狗 直接写 name狗 狗在啃骨头 动物在吃东西这里有个值得注意的细节字段的“同名覆盖”在 Java 里叫变量隐藏variable hiding它和方法重写有本质区别。方法重写是动态分派的运行期看对象实际类型而字段访问是编译期静态绑定的写super.name就访问父类字段写this.name就访问子类字段。顺便说一句日常开发里我不太建议刻意搞字段隐藏因为可读性太差了。真到了需要区分的时候更好的做法是父类字段用private再加protected的 getter/setter子类通过方法访问。这样既安全又清晰super 只需要留在重写方法这个场景里用就够了。2.2 用 super() 调用父类构造器super 最常见的用法其实是super(...)这种形式用来调用父类的构造器。class Animal { private String name; public Animal(String name) { this.name name; } } class Dog extends Animal { public Dog(String name) { super(name); // 必须放在第一行 } }写super(参数)本质上就是在子类构造器里把初始化父类那部分字段的任务委托给父类的某个构造器。这个用法还引出了一个很多新手踩坑的规则如果子类构造器里没有显式写 super(...)编译器会自动塞一个无参的super()。这条规则最典型的翻车场景是父类只写了带参构造器、没写无参构造器子类也没写任何构造器。这时候编译直接失败因为编译器想帮你调用super()但父类根本没有这个构造器。很多初学者第一次遇到这个报错会一脸懵其实背后就是这么回事。2.3 在重写方法里通过 super 复用父类逻辑第三大用法是在子类重写的方法里调用super.方法名()拿到父类版本的实现然后在其基础上扩展。这是面向对象设计里非常经典的做法也是模板方法模式的底层支撑。class BaseService { public void save() { System.out.println(校验参数); System.out.println(写入数据库); } } class UserService extends BaseService { Override public void save() { super.save(); // 先复用父类的通用逻辑 System.out.println(记录操作日志); } }这样父类负责公共流程子类只补自己的特殊逻辑代码不会重复职责也清楚。另外要记住一个点super 只能用在实例方法、构造器和实例初始化块里静态方法、静态块里不能出现 super。原因前面说过——super 本质是访问“当前对象里的父类区域”而静态上下文里根本没有对象实例。3. 构造器链与 super() 的完整规则3.1 为什么 super() 必须是构造器第一行这是我被问过很多次的问题标准答案其实一句话就能说清楚父类部分必须先初始化完成子类代码才能安全地使用它。想想看子类构造器里可能随时会访问父类声明的字段如果父类构造器还没运行那些字段就是默认值甚至还没准备妥当。Java 为了保证“先父后子”的初始化顺序干脆用语法强制super(...)必须是构造器里的第一条可执行语句。如果允许你把 super() 写在后面那this.name、父类字段这些访问就可能在父类还没初始化完的情况下发生整个对象的状态会处于一种不可预测的中间态。这跟盖楼的逻辑一模一样地基没打牢二楼不能开始砌墙。3.2 子类构造器里到底发生了三次“调用接力”为了看得清楚我们用一组带输出语句的类来观察class GrandParent { GrandParent() { System.out.println(1. GrandParent 构造器); } } class Parent extends GrandParent { Parent() { System.out.println(2. Parent 构造器); } } class Child extends Parent { Child() { System.out.println(3. Child 构造器); } } public class Demo { public static void main(String[] args) { new Child(); } }输出顺序是1. GrandParent 构造器 2. Parent 构造器 3. Child 构造器虽然我们在 Child 构造器里什么都没写但编译器自动补了super()Parent 构造器里也隐式补了super()。于是 new 一个 Child实际是从最顶层的父类一路往下执行像接力棒一样传回子类。完整顺序还要分得更细类加载时先执行父类的静态初始化块再执行子类的静态初始化块创建对象时先执行父类的实例初始化块和构造器再执行子类的实例初始化块和构造器。所以一个多层继承的对象创建过程永远遵循“静态先行、父类先行”的总原则。3.3 父类没有无参构造器时的处理方案上面提到的编译失败场景解决办法无非两种。第一种是父类补一个无参构造器第二种是子类显式调用父类的带参构造器。class Animal { private String name; public Animal(String name) { this.name name; } } class Dog extends Animal { public Dog(String name) { super(name); // 显式调用编译通过 } }这个场景在封装工具类、基类设计时特别常见。父类为了保证必填字段只提供带参构造器子类就必须老老实实把参数透传上去。如果你看到一个报错信息是类似Implicit super constructor Animal() is undefined不用怀疑就是父类缺无参构造器或者子类没显式调带参的父类构造器。4. 实战中的高频问题与排查心得4.1 高级陷阱super 调用父类方法真的能绕过多态吗这个问题是面试里的分水岭。先看代码class Parent { public void run() { System.out.println(Parent.run); others(); } public void others() { System.out.println(Parent.others); } } class Child extends Parent { Override public void others() { System.out.println(Child.others); } public void callParent() { super.run(); // 显式调用父类 run } } public class Demo { public static void main(String[] args) { new Child().callParent(); } }猜猜输出是什么很多人会以为super.run()直接调父类实现那父类 run 里调用的 others() 也应该走 Parent 自己的版本输出两行“Parent”。但实际输出是Parent.run Child.others这个结果让很多人栽过跟头。原因在于super.run()确实绕过了动态分派直接执行了 Parent 里 run 方法的字节码但 run 方法内部调用 others() 时方法调用指令是invokevirtual它按照“实际运行的对象类型”来分派。当前对象的实际类型是 Child所以 others() 会命中 Child 的重写版本。一句话总结super 能让你跳过当前层的重写直接执行上一层的方法但那个方法内部对其他方法的调用仍然会按照多态规则分派到最具体的子类实现。这说明多态是深入对象骨髓的机制不是靠一个 super 就能完全绕开的。4.2 构造器里调用可重写方法一颗定时炸弹正因为多态无法被 super 完全按住所以在构造器里调用一个可能被子类重写的方法是极度危险的操作。class Parent { public Parent() { init(); } protected void init() { System.out.println(Parent.init); } } class Child extends Parent { private String name 张三; Override protected void init() { System.out.println(Child.init, name name); } } public class Demo { public static void main(String[] args) { new Child(); } }执行到new Child()时先进入 Parent 的构造器调用 this.init()因为动态分派实际执行的是 Child 的 init()。但此时 Child 的字段 name 还没有被赋值输出是Child.init, name null。如果 init() 里再对 name 调用别的方法直接就是空指针。这个问题的根源就是构造器里发生了“父类构造阶段调用子类方法”的错位。实际开发中的确有人这么写过排查起来异常痛苦。我的建议很简单构造器里只做字段初始化不要调用任何非 final 的、可被重写的方法如果必须调用就把那个方法标成 final或者在子类字段初始化完成后通过别的方式补接。4.3 super 与 this 混用时的规则有些新手会在一个构造器里同时写 this(...) 和 super(...)直接被编译错误拦下。原因不难理解这两条语句都要求自己是第一行它们不可能同时满足。实际项目里常见的写法是 this(...) 调本类其它构造器被调的那个构造器再调 super()。比如class Dog extends Animal { public Dog() { this(未命名); } public Dog(String name) { super(name); } }这种写法完全合法因为 this() 只是把执行流程转交给另一个构造器最终那个构造器仍然会调用 super()。构造器链的“先父后子”保证不会因此被破坏。要记住的是不管中间怎么传递整个实例创建过程里 super() 一定被调用且只会被调用一次。4.4 高频考点速查表场景结论super 是不是引用变量不是不能赋值、不能传参、不能单独使用super() 的位置必须位于构造器第一行子类构造器不写 super()编译器自动补无参 super()父类没有就会编译失败父类没有无参构造器子类必须显式调用父类带参构造器静态方法里能用 super 吗不能super.方法() 调用重写方法直接执行父类版本不触发动态分派父类方法内部调用重写方法仍然多态分派到子类实现构造器里调用可重写方法强烈不建议可能拿到未初始化字段5. 完整案例模拟一个三层继承的交通工具体系5.1 场景设计与代码实现前面把规则都过了一遍这里用一个完整的三层继承案例串起来Vehicle交通工具→ Car汽车→ ElectricCar电动车。每一层都带自己的字段、构造器和 run() 方法子类构造器层层传递参数子类方法通过 super 调用父类实现。class Vehicle { protected String brand; protected int wheels; public Vehicle(String brand, int wheels) { this.brand brand; this.wheels wheels; System.out.println(Vehicle 构造器 brand , wheels 轮); } public void run() { System.out.println(brand 正在行驶); } } class Car extends Vehicle { protected String model; public Car(String brand, int wheels, String model) { super(brand, wheels); this.model model; System.out.println(Car 构造器 model); } Override public void run() { System.out.println(汽车 model 准备启动); super.run(); } } class ElectricCar extends Car { private int batteryLevel; public ElectricCar(String brand, int wheels, String model, int batteryLevel) { super(brand, wheels, model); this.batteryLevel batteryLevel; System.out.println(ElectricCar 构造器电量 batteryLevel %); } Override public void run() { System.out.println(电动车 model 电量 batteryLevel %); super.run(); } } public class Demo { public static void main(String[] args) { ElectricCar car new ElectricCar(某品牌, 4, 某型号, 80); car.run(); } }5.2 运行输出与执行过程分析输出结果如下Vehicle 构造器某品牌, 4轮 Car 构造器某型号 ElectricCar 构造器电量 80% 电动车 某型号 电量 80% 汽车 某型号 准备启动 某品牌 正在行驶前半段是构造器输出清晰地展示了调用链new ElectricCar 时先走 Vehicle 构造器再走 Car 构造器最后才轮到 ElectricCar 自己。这就是 super() 隐式调用形成的自顶向下初始化链路。后半段是方法输出展示了动态调用链ElectricCar.run() 里先打印自己的信息然后 super.run() 跳到 Car.run()Car 里又调 super.run() 跳到 Vehicle.run()。整个执行过程像洋葱一样一层层剥开每一层都先做自己的事再往父类传递。这个案例里有个细节值得体会ElectricCar.run() 里的 super.run() 调用的是 Car.run()而不是 Vehicle.run()。因为 super 是“直接父类”的访问入口不是“最顶层父类”。想要跳过 Car 直接调 Vehicle 的方法是不可能的这也是 super 和很多人的直觉不一样的地方。5.3 模板方法模式super 在企业代码里的典型身影最后补充一个真实项目里非常常见的场景。设计一个数据解析框架父类定义好不可变的处理流程骨架子类实现具体步骤而且子类重写步骤方法时可以调用 super 复用父类默认实现abstract class DataParser { // 模板方法算法骨架不允许子类改变 public final void parse() { readData(); processData(); writeResult(); } protected abstract void readData(); protected abstract void processData(); protected void writeResult() { System.out.println(将结果写出到默认位置); } } class CsvParser extends DataParser { Override protected void readData() { System.out.println(读取 CSV 文件); } Override protected void processData() { System.out.println(解析 CSV 行数据); } Override protected void writeResult() { super.writeResult(); // 复用父类默认写出逻辑 System.out.println(额外补充 CSV 表头信息); } }这种设计里super 的价值体现得淋漓尽致子类在不修改流程的前提下通过调用父类方法复用通用逻辑再叠加自己的增强逻辑。框架代码的扩展点因此变得非常干净。根据我个人在实际项目中的体会super 相关的 bug 大多数不是语法问题而是“脑子里以为的调用目标和实际执行目标不一致”。排查这类问题最有效的办法一是搞清楚当前对象的实际类型二是把调用链上的每个类的构造器和方法都打上日志看输出顺序。一旦你理解了 super 不是独立引用、构造链必然自顶向下、多态永远看实际类型这三点Java 继承体系的大半知识点就都通了。