本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。
点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!
本文篇幅较长,建议先收藏再食用!
系列文章目录
JAVA学习 DAY2 java程序运行、注意事项、转义字符
JAVA学习 DAY5 变量&数据类型 [万字长文!一篇搞定!]
JAVA学习 DAY7 程序逻辑控制【万字长文!一篇搞定!】
JAVA学习 DAY11 类和对象_续1【万字长文!一篇搞定!】
JAVA学习 DAY12 继承和多态【万字长文!一篇搞定!】
JAVA学习 DAY13 抽象类和接口【万字长文!一篇搞定!】
目录
Java避坑指南:千万别在构造方法中调用重写的方法!(附代码案例+执行流程全解析)
步骤3:执行子类D的func()方法,此时子类成员变量未初始化
前言
小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!
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
看到这个结果,很多人可能会有两个疑问:
-
为什么执行的是子类D的func()方法,而不是父类B的func()方法?毕竟func()是在父类B的构造方法中调用的啊!
-
子类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中“子类对象创建时的初始化顺序”(重点中的重点,务必记住):
-
加载并初始化父类的静态成员(如果有);
-
加载并初始化子类的静态成员(如果有);
-
执行父类的实例变量初始化(如B类若有实例变量,先初始化);
-
执行父类的构造方法;
-
执行子类的实例变量初始化(本文中就是num=1);
-
执行子类的构造方法(剩余逻辑)。
回到我们的案例中:
当执行到子类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面试中的高频考点,除了案例本身,面试官还可能会延伸问以下问题,大家可以提前准备:
-
Q:Java中对象创建的初始化顺序是什么?(答案就是我们前面梳理的6个步骤,重点区分静态初始化和实例初始化的顺序)
-
Q:多态的动态绑定机制是在编译期还是运行期生效?(运行期生效,编译期只做语法校验)
-
Q:final、private、static修饰的方法,能否被重写?(都不能,final禁止重写,private子类不可见,static是类方法,只能隐藏)
-
Q:如果父类构造方法中调用的是抽象方法(父类是抽象类),会发生什么?(子类必须重写抽象方法,执行流程和本文案例一致,子类未初始化时调用抽象方法的重写实现,同样可能访问到未初始化的成员变量)
七、总结
本文通过一个简单的代码案例,深入拆解了“父类构造方法中调用重写方法”的问题本质,核心要点总结如下:
-
父类构造中调用重写方法,会触发动态绑定,执行子类的重写实现;
-
子类成员变量初始化在父类构造执行完毕之后,此时调用子类方法会拿到默认值;
-
避坑核心:不调用可重写方法,明确初始化顺序,不依赖子类状态;
-
牢记对象初始化顺序和多态动态绑定机制,是解决这类问题的关键。

希望本文能帮大家彻底搞懂这个Java避坑点,在实际开发中少走弯路。如果觉得有帮助,欢迎点赞、收藏、转发,也欢迎在评论区留言讨论~
总结
以上就是今天要讲的内容,本文简单记录了JAVA学习笔记,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!
1561

被折叠的 条评论
为什么被折叠?



