为何无法从父类构造器访问子类构造参数?及解决方案
问题分析与解决方案
为什么会出现NPE?
当创建子类B的实例时,基于JVM的初始化顺序规则如下:
- 优先执行父类
A的构造逻辑,包括init块中的代码; - 父类
init块调用了抽象方法f(),而该方法被子类B重写; - 此时子类
B的成员属性p还未完成初始化——子类的所有成员(包括主构造中声明的val/var参数),都要等父类构造完全结束后才会赋值; - 因此在
B的f()方法中访问p时,它还是null,调用p()自然抛出NullPointerException。
解决方案
方案一:将父类初始化逻辑改为手动调用的方法
把父类init块中的逻辑抽成独立方法,在子类实例创建完成后再调用,确保子类成员已经初始化:
abstract class A { // 原init逻辑改为公开的初始化方法 fun initialize() { f() } abstract fun f() } class B(val p: () -> Unit) : A() { override fun f() { try { p() } catch (c: Throwable) { println(c) } } } // 创建实例后手动触发初始化 val b = B { println("p() called") }.apply { initialize() }
这种方式直接规避了父类构造时访问子类未初始化成员的问题,逻辑清晰可控。
方案二:延迟子类成员初始化并做安全检查
使用lateinit标记成员,在调用前检查是否已初始化,避免NPE:
abstract class A { init { f() } abstract fun f() } class B : A() { lateinit var p: () -> Unit // 用次构造器接收参数,在父类构造完成后赋值 constructor(p: () -> Unit) : this() { this.p = p } override fun f() { // 检查p是否已初始化,再执行调用 if (::p.isInitialized) { try { p() } catch (c: Throwable) { println(c) } } } } val b = B { println("p() called") }
注意:该方案下,父类init调用f()时p还未初始化,因此不会执行p(),如果需要父类构造阶段就执行目标逻辑,此方案不适用。
方案三:调整父类设计,直接接收初始化逻辑
如果父类init块的作用就是执行某个初始化动作,可以把该动作作为参数传给父类,避免子类重写方法:
abstract class A(private val initAction: () -> Unit) { init { initAction() } } class B(val p: () -> Unit) : A(p) { // 若不需要额外逻辑,无需重写方法 } val b = B { println("p() called") }
这种方式从根源上避免了父类构造时访问子类未初始化成员的问题,代码更简洁。
内容的提问来源于stack exchange,提问作者Alexey
相关产品推荐
相关产品推荐

