Java this 与 super 详解|继承高频踩坑 + 面试题整理

大家好,这里是 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 底层原理、后端线上踩坑实录,系列文章整理成合集,方便系统学习。

相关推荐
萧瑟余晖1 小时前
Spring Cloud配置中心与动态刷新详解
spring cloud·面试
用户7771298509121 小时前
Stream流式使用
java
旋律4141 小时前
List、Set、Map:三个装数据的容器,到底啥区别?怎么遍历?
java·后端
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 回顾和今日内容介绍
java·开发语言·交友
工具派1 小时前
不重写一行老代码:给 Spring Boot 系统加一层 AI 能力
java·人工智能·spring boot
aiks_yuan1 小时前
RestTemplate 废弃后迁移 RestClient 实战:Spring Boot 4.2 升级指南
spring boot
大雄很用心1 小时前
Java 数据结构基础:栈、堆与 Deque
java
牛战士coding1 小时前
用户点了停止生成,上游还在烧 token 吗?取消传播链路拆解
java·人工智能
初心未改HD1 小时前
DSH Skill开发实战:SKILL.md是给LLM看的操作手册,不是给人看的README,踩了4个坑
java·大数据·开发语言