大家好,这里是 Java 后端基础拾光集。
前两天在技术群里看到一个兄弟吐槽,写继承代码编译报错,对着 IDE 翻了二十分钟才发现,是自己在构造方法第二行写了 super ()。
其实 this 和 super 这俩关键字,看起来是 Java 里最基础的东西,真写起继承来,踩坑的人一点都不少。继承里面,this 和 super 几乎是必考题,看着简单,但是构造调用、this ()、super () 互斥、子类父类初始化顺序,特别容易翻车。今天咱们就把这俩彻底掰明白,以后不管是写业务还是面试,都不会再在这里丢分。
高频面试问题:
super是父类对象的引用吗?
如果你回答"是",那这篇文章正好帮你把这个坑填上。因为 Java 里根本不存在一个独立的"父类对象" 。当前对象在内存里是一个整体,父类定义的字段和子类定义的字段都长在同一个对象身上。super 不是一个指向"父类对象"的引用,而是一个用来访问"当前对象中父类那部分成员"的关键字。
用厨房类比说:一个厨师身上有"学徒时期学到的基本功"和"后来自己练出来的新菜",super 指向的是他身上"学徒那部分",而不是另一个叫"父类厨师"的人。
关键前提:this () 和 super () 放在构造方法里的时候,必须是第一行,而且两个不能同时写
一、先搭个场景:后厨里只有一个厨师
想象你是一家餐厅的后厨。
- 对象 = 一个厨师。
- 父类 = 这个厨师学徒时期学的菜谱和规矩。
- 子类 = 这个厨师后来自己练的新菜和改过的做法。
this= 厨师拍着胸脯说"我",指当前这个正在干活的人。super= 厨师翻出"学徒时期那本菜谱",看当年师傅怎么教的。
关键点先记住:后厨里只有一个厨师,不是学徒一个、现在的他一个。 this 和 super 说的是同一个人身上的不同部分。
二、 this:当前对象自报家门(厨师说"我")
this 的本质很纯粹:当前对象的引用 。谁在调用这个方法,this 就指向谁。
厨房里,厨师说"我",指的就是当前这个正在干活的厨师本人,不是隔壁灶台的。
1. 区分成员变量(我的)和局部变量(你递过来的)
最常见的情况:别人递过来一个食材,名字和你自己柜子里的东西重名(变量同名)了。
typescript
public class User {
private String name; // 我自己柜子里的 name
public void setName(String name) { // 别人递过来的 name
this.name = name; // this.name 是我的,右边的 name 是递过来的
}
}
不加 this,name = name 就是"把递过来的东西又递回给自己",自己柜子里的 name 纹丝不动。这种 bug 编译器不报错,但行为一定不对。
好比: "把客人给的盐,放进我自己的盐罐里。" 不说清楚"我的盐罐",那就只是把盐从左手倒右手。
2. 在本类构造方法中,调用本类其他构造 this(参数)
this() 叫本店另一个灶台帮忙
一个厨师有几种做菜流程,但核心步骤差不多。与其每套流程都重写一遍,不如让一个流程直接喊另一个流程接手:
arduino
public class Order {
private String id;
private int amount;
public Order() {
this("default", 0); // 喊本店另一个构造流程接手,调用下面的双参构造器
}
public Order(String id, int amount) {
this.id = id;
this.amount = amount;
}
}
这个就是为了少写重复代码,比如很多类有好几个构造方法,冗余的初始化逻辑抽出来,用 this () 互相调用就行,不用把代码复制三遍。
⚠️ this () 只能写在构造器第一行,不能在普通方法里调用,而且不能自己调自己形成循环。
3. 返回当前对象本身(链式调用常用) return this;
做完一道菜,把"自己"交出去继续用
kotlin
public class QueryBuilder {
private String table;
public QueryBuilder from(String table) {
this.table = table;
return this; // 返回当前对象,继续点下去
}
}
new QueryBuilder().from("user").from("order") 这种链式写法,底层就是每次把"自己"交出去,下一道工序还是同一个人接着做。
4. 作为参数传递
把当前对象交给别的方法或注册到某个容器里:
kotlin
eventBus.register(this);
把自己注册到某个地方,让以后有事能找到"我"。这个用法本身没问题,但在构造器里这么干要格外小心,后面踩坑案例会讲。
5. 内部类(包间里的"我")中指回外部类(店里的"我")对象
在内部类里,this 默认指内部类自己。要指外部类对象,得写 Outer.this:
kotlin
public class Outer {
private String name = "outer";
class Inner {
private String name = "inner";
public void print() {
System.out.println(this.name); // inner
System.out.println(Outer.this.name); // outer
}
}
}
内部类就像后厨里隔出来的一个包间。包间里的厨师说"我",默认指包间里的自己;要指外面大厅的那个自己,得说"大厅的我"
三、super:不是"父类对象",是"父类那部分"
这是本文最想纠正的一个点。 super:翻出"学徒时期那本菜谱"
很多人把 super 理解成"指向父类对象的引用",于是会问出"super 和 this 是不是两个不同对象"这种问题。不是的。当前对象只有一个 ,this 指向它整体,super 是访问它身上"继承自父类的那部分成员"的入口。
用法 1:super() 调用父类构造器
子类构造器里,super(...) 用来调用父类构造器,先按师傅的规矩把底子打好,完成父类字段的初始化:
scala
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
public class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name); // 必须先初始化父类部分
this.breed = breed;
}
}
硬规则和 this() 一样:必须在构造器第一行 。如果子类构造器第一行既没写 this(...) 也没写 super(...),编译器会自动插入一个无参的 super()。如果父类没有无参构造器,编译直接报错------相当于师傅那本菜谱第一页就缺了,你没法照着开火。
用法 2:super.field 访问父类被隐藏的字段
翻出师傅那本被压箱底的菜谱
当子类声明了和父类同名的字段,父类字段被"隐藏"了。这时可以用 super.field 访问父类那份:
scala
public class Parent {
protected String name = "parent";
}
public class Child extends Parent {
private String name = "child";
public void print() {
System.out.println(this.name); // 我自己那本
System.out.println(super.name); // 师傅那本
}
}
注意措辞:字段叫隐藏(hiding) ,方法才叫重写(overriding) 。这是两个不同的概念。厨房话:同名菜谱放两本,叫"藏起来了";同一道菜做法改了,叫"重写了"。
用法 3:super.method() 调用父类被重写的方法
按师傅的老做法先来一遍
子类重写了父类方法后,如果想在子类实现里复用父类逻辑,用 super.method():
scala
public class Child extends Parent {
@Override
public void doWork() {
super.doWork(); // 先执行父类逻辑,先按师傅的老做法来一遍
// 再加子类自己的逻辑,自己的新做法
}
}
这里有一个面试常考的细节:super.method() 不是多态调用 。它不会走动态绑定,而是直接调用父类版本的实现。字节码层面用的是 invokespecial,不是 invokevirtual。所以哪怕子类又重写了,super.method() 调的也一定是父类那一版。"别管我现在自己怎么做,这道菜先按师傅当年教的来。"
👉 如果我们不手写 super (),编译器会默认在子类构造第一行补上无参 super ();父类没有无参构造,子类必须手动写带参 super,否则直接编译报错!
四、几条必须记住的硬规则
把 this 和 super 最容易踩的规则集中列一下:
| 规则 | 说明 |
|---|---|
this() 和 super() 都必须在构造器第一行 |
编译器强制 |
this() 和 super() 不能同时出现在同一个构造器里 |
第一行只能有一条 |
this() 不能形成循环调用 |
比如 A 调 B、B 又调 A,编译报错 |
静态方法里不能用 this 和 super |
静态方法不属于任何对象 |
super 不能单独作为值使用 |
不能 return super;,它不是引用 |
| 字段是隐藏,方法是重写 | super.field 访问父类字段,super.method() 调父类方法 |
五、三个踩坑案例,都是真实代码里会遇到的
事故 1:师傅在教徒弟时喊了徒弟的新做法,结果徒弟还没准备好
csharp
public class Parent {
public Parent() {
init(); // 师傅在构造时喊了一嗓子
}
protected void init() {
System.out.println("Parent init");
}
}
public class Child extends Parent {
private String value = "child-value";
@Override
protected void init() {
System.out.println("Child init, value = " + value);
}
}
执行 new Child(),输出是:
csharp
Child init, value = null
为什么?父类构造器先于子类字段初始化执行。 super() 跑的时候,子类字段 value 还是默认值 null。init() 被动态绑定到了子类版本,但子类那部分还没准备好。
厨房话:师傅在教基本功时喊"按新菜谱来",可徒弟的新菜谱还没写呢,只能念出个空本子。
正确姿势 :构造器里尽量只做字段赋值,不要调用可被重写的方法;确实需要钩子,就延迟到对象构造完成后再调,或者把方法声明为 final/private。
事故 2:菜还没做完,就把"自己"交出去了
csharp
public class Task {
public Task() {
Executors.newSingleThreadExecutor().submit(() -> {
System.out.println(this.toString()); // this 提前暴露
});
}
}
构造器还没执行完,this 就被交给了线程池。另一个线程可能在对象尚未完成初始化 时就开始用它,读到半成品状态。把 this 注册到事件总线、放进静态集合、启动线程,都属于这类"this 逃逸"。
厨房话:菜还在锅里,就把盘子递给传菜员了,传菜员端出去的可能是半生不熟的。
正确姿势:对象构造完成后再对外暴露引用;或者用工厂方法在构造结束后统一注册。
事故 3:想同时喊两个人帮忙,编译器不让
scss
public Child() {
super(); // 第一句
this("default"); // 第二句,编译报错
}
两个调用都要求在第一句,天然互斥。需要复用构造逻辑时,让 this() 链最终落到那个调 super() 的构造器上,而不是在同一个构造器里两个都写。
厨房话:开火前只能喊一个人来搭手,不能同时喊师傅和另一个灶台。真要都喊,就让其中一个去喊另一个。
六、高频大坑(面试最爱问)
Q1:this () 和 super () 不能共存 ?
A: 两个都是构造调用,都要求放在第一行,写在一起直接报错。
Q1:子类初始化顺序?
A : 子类初始化顺序:父类先初始化,再子类 先父构造、再子构造。厨房要先搭好基础厨房(父),才能加装子类设备。
Q1:子类构造器不写 super() 会怎样?
A: 编译器自动补一个无参 super();如果父类没有无参构造器,编译报错。
Q1:静态方法里为什么不能用 this 和 super?
A: 静态方法属于类,不属于任何一个厨师,是贴在墙上的公共规矩,没有"我"可言。
Q1:字段隐藏和方法重写有什么区别?
A: 子类同名字段会藏起父类字段,this.field 和 super.field 分别翻两本;方法重写是多态,super.method() 可以显式翻父类那版。
七、核心总结
用厨房收个尾:
this是厨师说"我" ,指当前这个正在干活的人,用来分清"我的"和"别人递过来的";super不是"另一个厨师" ,而是这个厨师身上"学徒时期学的那部分本领",用来在需要时翻出师傅那本菜谱。
再回到开头那个问题:
super是父类对象的引用吗?
不是。后厨里只有一个厨师,this 指他整个人,super 指他脑子里"学徒那部分"。 把这句话记住,this 和 super 的绝大多数用法和坑,都能顺着推出来。
最后:
this 代表当前实例对象,super 代表父类的对象引用;
this 可以访问本类属性、方法、调用本类构造;
super 访问父类属性、方法、调用父类构造;
构造方法内 this ()、super () 都要求写在第一行,二者不能同时出现;
静态上下文不能使用 this 和 super。
📝本文属于专栏【Java 后端基础拾光集】,生活化讲解 Java 基础,持续更新面试知识点。 公众号【Java后端程序媛手记】同步更新 Java 底层原理、后端线上踩坑实录,系列文章整理成合集,方便系统学习。