Java避坑指南:千万别在构造方法中调用重写的方法!(附代码案例+执行流程全解析)

 本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。

点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励! 

本文篇幅较长,建议先收藏再食用!


 系列文章目录

JAVA学习 DAY1 初识JAVA

JAVA学习 DAY2 java程序运行、注意事项、转义字符

JAVA学习 DAY3 注释与编码规范讲解

JAVA学习 DAY4 DOS操作讲解及实例

JAVA学习 DAY5 变量&数据类型 [万字长文!一篇搞定!] 

JAVA学习 DAY6 运算符

JAVA学习 DAY7 程序逻辑控制【万字长文!一篇搞定!】

JAVA学习 DAY8 方法【万字长文!一篇搞定!】

JAVA学习 DAY9 数组【万字长文!一篇搞定!】

JAVA学习 DAY10 类和对象【万字长文!一篇搞定!】

JAVA学习 DAY11 类和对象_续1【万字长文!一篇搞定!】

JAVA学习 DAY12 继承和多态【万字长文!一篇搞定!】

JAVA学习 DAY13 抽象类和接口【万字长文!一篇搞定!】


目录

 系列文章目录

前言

Java避坑指南:千万别在构造方法中调用重写的方法!(附代码案例+执行流程全解析)

一、完整代码案例

二、执行结果及疑问

三、执行流程全解析(重中之重)

步骤1:触发子类D的对象创建,进入默认构造方法

步骤2:执行父类B的构造方法,触发func()方法调用

步骤3:执行子类D的func()方法,此时子类成员变量未初始化

步骤4:父类构造方法执行完毕,子类完成初始化

四、执行流程总结(图文化梳理)

五、问题本质及避坑技巧

技巧1:绝对不要在父类构造方法中调用“可被重写”的方法

技巧2:明确对象初始化顺序,避免在构造方法中依赖子类状态

技巧3:使用@Override注解,明确方法重写场景

六、常见面试延伸问题

七、总结

总结


前言

小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!

Java避坑指南:千万别在构造方法中调用重写的方法!(附代码案例+执行流程全解析)

今天给大家分享一个Java开发中非常容易踩的“坑”——在父类构造方法中调用被子类重写的方法。这个场景看似简单,实则隐藏着多态动态绑定和对象初始化顺序的深层逻辑,很多初学者甚至有一定经验的开发者都会在这里栽跟头。

本文将通过一个完整的代码案例,从执行结果、初始化顺序、动态绑定机制三个维度,一步步拆解这个问题的本质,最后总结避坑技巧,帮大家彻底搞懂背后的原理,避免在实际开发中出现类似bug。

先上完整代码案例,大家可以先自己思考一下执行结果会是什么~

一、完整代码案例

package demo6;
//避免在构造方法中调用重写的方法
class B {
    //父类的构造方法
    public B() {
        // do nothing
        func(); // 父类构造方法中调用func()方法
    }
    
    // 普通成员方法(非构造方法,构造方法必须和类名完全相同)
    public void func() {
        System.out.println("B.func()");
    }
}

class D extends B {
    private int num = 1; // 子类的实例变量,初始化值为1
    
    // D类没有显式定义构造方法,JVM自动生成默认构造方法
    // 默认构造方法隐含逻辑如下:
    // public D() {
    //     super(); // 调用父类B的构造方法(必须是第一条语句)
    //     // 子类成员变量初始化、其他逻辑...
    // }
    
    // 重写父类B的func()方法
    @Override
    public void func() {
        System.out.println("D.func() " + num);
    }
}

public class Test {
    public static void main(String[] args) {
        D d = new D(); // 创建子类D的对象
    }
}

二、执行结果及疑问

先公布执行结果:

执行结果:D.func() 0

看到这个结果,很多人可能会有两个疑问:

  1. 为什么执行的是子类D的func()方法,而不是父类B的func()方法?毕竟func()是在父类B的构造方法中调用的啊!

  2. 子类D的num变量初始化值是1,为什么输出的是0?

要搞懂这两个问题,核心需要掌握两个Java基础知识点:对象初始化顺序多态的动态绑定机制。下面我们一步步拆解执行流程,逐个解答疑问。

三、执行流程全解析(重中之重)

当我们执行D d = new D();创建子类对象时,Java的执行流程并不是简单的“子类构造方法执行”,而是有严格的初始化顺序。我们按照时间线一步步拆解,每个步骤都标注清楚关键逻辑和变量状态。

步骤1:触发子类D的对象创建,进入默认构造方法

由于子类D没有显式定义构造方法,JVM会自动生成一个默认构造方法。默认构造方法的核心逻辑只有一条:调用父类的无参构造方法(super()),并且super()必须是子类构造方法的第一条语句(无论显式写不写,JVM都会自动执行)。

此时子类D的默认构造方法隐含逻辑:

public D() {
    super(); // 第一步:调用父类B的构造方法
    // 后续步骤:子类成员变量初始化(num=1)、其他逻辑...
}

步骤2:执行父类B的构造方法,触发func()方法调用

执行super()后,程序进入父类B的构造方法public B()。父类构造方法中只有一行代码:func();

这里就是第一个关键节点:父类构造方法中调用了func()方法,而这个方法被子类D重写了。此时会触发Java的多态动态绑定机制,我们分“编译期”和“运行期”两个阶段来看:

  • 编译期校验:编译器在编译父类B的构造方法时,只关注当前上下文(B类)。由于B类中确实定义了func()方法(非抽象方法),语法上合法,因此编译通过。此时编译器并不知道后续会有子类重写这个方法,也不会去校验子类的实现。

  • 运行期动态绑定:JVM在运行时,会识别当前正在创建的对象的“实际类型”——虽然现在执行的是父类B的构造方法,但正在创建的对象是子类D的实例(new D())。根据Java多态的动态绑定规则:当调用一个被重写的方法时,实际执行的是对象实际类型(子类)的重写实现,而非父类的原方法

因此,父类构造方法中的func();,最终执行的是子类D重写后的func()方法,而不是父类B的func()方法。

步骤3:执行子类D的func()方法,此时子类成员变量未初始化

这是第二个关键节点,也是导致num输出0的核心原因:子类的成员变量初始化,是在父类构造方法执行完毕之后才进行的

我们先明确Java中“子类对象创建时的初始化顺序”(重点中的重点,务必记住):

  1. 加载并初始化父类的静态成员(如果有);

  2. 加载并初始化子类的静态成员(如果有);

  3. 执行父类的实例变量初始化(如B类若有实例变量,先初始化);

  4. 执行父类的构造方法;

  5. 执行子类的实例变量初始化(本文中就是num=1);

  6. 执行子类的构造方法(剩余逻辑)。

回到我们的案例中:

当执行到子类D的func()方法时,父类B的构造方法还在执行中(并未执行完毕),此时步骤5(子类实例变量初始化)还未进行。子类D的num变量是int类型,Java中基本数据类型的默认值是0(int默认0,double默认0.0,boolean默认false等),因此此时num的值还是默认值0,而非初始化值1。

所以,子类D的func()方法执行时,输出的是D.func() 0

步骤4:父类构造方法执行完毕,子类完成初始化

子类D的func()方法执行完毕后,程序回到父类B的构造方法,父类构造方法执行完毕(因为只有func()一行代码)。

此时,程序才会执行步骤5和步骤6:执行子类D的实例变量初始化(num=1),然后执行子类D默认构造方法的剩余逻辑(本文中没有其他逻辑)。

也就是说,直到整个对象创建完成,num的值才会变成1,但此时我们的func()方法已经在父类构造中执行过了,拿到的只是初始化前的默认值。

四、执行流程总结(图文化梳理)

为了方便大家记忆,我把整个执行流程整理成一个清晰的步骤表,一目了然:

执行步骤

执行内容

关键状态

输出结果

1

new D(),进入子类D默认构造方法

子类D构造方法隐含super(),准备调用父类B构造

2

执行super(),进入父类B构造方法

父类构造开始执行,准备调用func()

3

父类B构造中调用func(),触发动态绑定

运行时识别对象实际类型为D,执行D的func()

4

执行子类D的func()方法

子类num未初始化,值为int默认值0

D.func() 0

5

子类D的func()执行完毕,回到父类B构造

父类B构造执行完毕

6

回到子类D构造,执行num=1初始化

子类num值变为1

7

子类D默认构造方法执行完毕,对象创建完成

num=1,但已无输出逻辑

五、问题本质及避坑技巧

通过上面的分析,我们可以总结出这个“坑”的本质:

父类构造方法执行时,子类对象尚未完成初始化(成员变量未赋值),此时调用被子类重写的方法,会导致子类方法访问到未初始化的成员变量(只能拿到默认值),从而引发逻辑错误或数据异常。

那么,在实际开发中,我们该如何避免这个问题呢?分享3个核心避坑技巧:

技巧1:绝对不要在父类构造方法中调用“可被重写”的方法

这是最核心的原则。如果父类构造方法中必须调用某个方法,务必确保这个方法是“不可被重写”的。如何保证?

  • private修饰方法:private方法不能被子类重写,父类构造中调用private方法,执行的一定是父类自身的实现;

  • final修饰方法:final方法不能被子类重写,同样能保证执行的是父类的实现;

  • static修饰方法:static方法属于类级别的方法,不会被重写(只能被隐藏),父类构造中调用static方法,执行的是父类的静态方法。

修改示例(用private修饰func(),避免被重写):

class B {
    public B() {
        func(); // 调用private方法,不会被重写
    }
    
    // private修饰,子类无法重写
    private void func() {
        System.out.println("B.func()");
    }
}

class D extends B {
    private int num = 1;
    
    // 这里不是重写,而是子类自己的方法(父类func()是private,子类看不到)
    public void func() {
        System.out.println("D.func() " + num);
    }
}
// 执行结果:B.func()(父类构造中调用的是自身的func())

技巧2:明确对象初始化顺序,避免在构造方法中依赖子类状态

牢记“父类构造先执行,子类初始化后执行”的原则,父类构造方法中不要依赖子类的任何状态(包括子类的成员变量、子类的重写方法等)。父类构造只负责初始化父类自身的状态,子类的状态由子类自己的构造方法和初始化逻辑负责。

如果父类需要用到子类的某些配置,可以通过“构造方法参数”的方式传递,而不是直接调用子类方法。例如:

class B {
    private int value;
    
    // 父类构造通过参数接收子类的配置,而非调用子类方法
    public B(int value) {
        this.value = value;
        System.out.println("B.func() " + value);
    }
}

class D extends B {
    private int num = 1;
    
    public D() {
        super(num); // 子类初始化后,将num作为参数传递给父类构造
    }
}
// 执行结果:B.func() 1(此时num已初始化,传递的是正确的值)

技巧3:使用@Override注解,明确方法重写场景

在子类重写父类方法时,一定要加上@Override注解。这个注解不仅能提高代码可读性,让开发者一眼看出这是重写方法,还能让编译器帮我们校验:确保父类中存在该方法(且访问权限允许重写),避免因拼写错误等导致“伪重写”(比如把func()写成fun(),编译器不会提示错误,但实际没有重写)。

虽然这个注解不能直接避免“构造方法调用重写方法”的问题,但能帮助我们更清晰地梳理类之间的方法关系,减少潜在风险。

六、常见面试延伸问题

这个知识点是Java面试中的高频考点,除了案例本身,面试官还可能会延伸问以下问题,大家可以提前准备:

  1. Q:Java中对象创建的初始化顺序是什么?(答案就是我们前面梳理的6个步骤,重点区分静态初始化和实例初始化的顺序)

  2. Q:多态的动态绑定机制是在编译期还是运行期生效?(运行期生效,编译期只做语法校验)

  3. Q:final、private、static修饰的方法,能否被重写?(都不能,final禁止重写,private子类不可见,static是类方法,只能隐藏)

  4. Q:如果父类构造方法中调用的是抽象方法(父类是抽象类),会发生什么?(子类必须重写抽象方法,执行流程和本文案例一致,子类未初始化时调用抽象方法的重写实现,同样可能访问到未初始化的成员变量)

七、总结

本文通过一个简单的代码案例,深入拆解了“父类构造方法中调用重写方法”的问题本质,核心要点总结如下:

  • 父类构造中调用重写方法,会触发动态绑定,执行子类的重写实现;

  • 子类成员变量初始化在父类构造执行完毕之后,此时调用子类方法会拿到默认值;

  • 避坑核心:不调用可重写方法,明确初始化顺序,不依赖子类状态;

  • 牢记对象初始化顺序和多态动态绑定机制,是解决这类问题的关键。

希望本文能帮大家彻底搞懂这个Java避坑点,在实际开发中少走弯路。如果觉得有帮助,欢迎点赞、收藏、转发,也欢迎在评论区留言讨论~


总结

以上就是今天要讲的内容,本文简单记录了JAVA学习笔记,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yvonne爱编码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值