我试图了解以下代码抛出的原因:
open class Base(open val input: String) {
lateinit var derived: String
init {
derived = input.toUpperCase() // throws!
}
}
class Sub(override val input: String) : Base(input)
当调用这样的代码时:
println(Sub("test").derived)
它会抛出异常,因为在调用toUpperCase
时,input
会解析为null
。我发现这个反直觉:我将一个非null值传递给主构造函数,但是在超类的init块中它解析为null?
我想我对可能发生的事情有一个模糊的概念:因为input
既是构造函数参数又是属性,分配内部调用this.input
,但是{{1}尚未完全初始化。这真的很奇怪:在IntelliJ调试器中,this
正常解析(到值#34; test"),但只要我调用表达式评估窗口并检查{{1}手动地,它突然变为空。
假设这是预期的行为,你建议做什么,即当需要初始化从同一类的属性派生的字段时?
更新 我发布了两个更简洁的代码片段,说明了混淆源自何处:
https://gist.github.com/mttkay/9fbb0ddf72f471465afc https://gist.github.com/mttkay/5dc9bde1006b70e1e8ba
答案 0 :(得分:8)
原始示例等同于以下Java程序:
class Base {
private String input;
private String derived;
Base(String input) {
this.input = input;
this.derived = getInput().toUpperCase(); // Initializes derived by calling an overridden method
}
public String getInput() {
return input;
}
}
class Derived extends Base {
private String input;
public Derived(String input) {
super(input); // Calls the superclass constructor, which tries to initialize derived
this.input = input; // Initializes the subclass field
}
@Override
public String getInput() {
return input; // Returns the value of the subclass field
}
}
在Sub类中重写了getInput()方法,因此代码调用Sub.getInput()。此时,Sub类的构造函数尚未执行,因此保存Sub.input值的后备字段仍为null。这不是Kotlin的错误;您可以轻松地在纯Java代码中遇到同样的问题。
修复方法是不覆盖该属性。 (我已经看过你的评论,但这并没有真正解释为什么你认为你需要覆盖它。)
答案 1 :(得分:3)
这种混淆来自于您为input
值创建了两个存储(JVM中的字段)。一个是基类,一个是派生的。当您在基类中读取input
值时,它会调用虚拟getInput
方法。在派生类中重写getInput
以返回其自己的存储值,该值在调用基本构造函数之前未初始化。这是构造函数中的典型"虚拟调用"问题。
如果你改变派生类实际使用超类型的属性,一切都很好。
class Sub(input: String) : Base(input) {
override val input : String
get() = super.input
}