初始化顺序2

Q:父类或者子类重写的值为什么会为空?
详细的例子:

abstract class A {
  val x1: String
  val x2: String = "mom"
  println("A: " + x1 + ", " + x2)
}
class B extends A {
  val x1: String = "hello"
  println("B: " + x1 + ", " + x2)
}
class C extends B {
  override val x2: String = "dad"
  println("C: " + x1 + ", " + x2)
}
object A extends App {
  new C
}

输出

A: null, null
B: hello, null
C:hello, dad

只有到了初始化C的构造器的时候,x1和x2才被初始化。因此,当A和B构造器初始化的时候,将会有NullPointerException的风险。

解释:
一个没有被lazy修饰的val变量,将会快速定义。

在没有声明并定义的情况下,完成初始化一个vals的变量将按照下面的顺序:

1.父类先进行完全初始化,才轮到子类
2.否则,将按照变量声明的顺序

一般来说,当一个val变量被子类重写,将会被多次初始化。因此在上面的例子中,x2在每个声明的地方都会定义。这是一个规则。

一个被子类重写的val变量,在父类构造器进行构造的时候会被初始化为null。

小提示:使用编译器参数能够有效地进行分辨初始化顺序。
-Xcheckinit:增加字段属性的运行时检查
使用此参数进行外部测试是不理智的。因为这会使用包装器对字未初始化段属性进行修饰,增加相当多的代码。
当val变量未被初始化,包装器将会抛出异常而不是让它默默出现。另外一个需要注意的是,这也是会增加运行时检查:当你在某处对为此变量进行结合的时候,它将会告诉你关于代码路径的任何事情。

使用案例:

% scalac -Xcheckinit a.scala
% scala -e 'new C'
scala.UninitializedFieldError: Uninitialized field: a.scala: 13
 at C.x2(a.scala:13)
 at A.(a.scala:5)
 at B.(a.scala:7)
 at C.(a.scala:12)

避免出现null值的处理方案

使用lazy进行修饰val变量

abstract class A {
  val x1: String
  lazy val x2: String = "mom"
  println("A: " + x1 + ", " + x2)
}
class B extends A {
  lazy val x1: String = "hello"
  println("B: " + x1 + ", " + x2)
}
class C extends B {
  override lazy val x2: String = "dad"
  println("C: " + x1 + ", " + x2)
}
object A extends App {
  new C
}

通常来说,这是最好的解决方案。不过不幸的是,你不能声明一个抽象的lazy val变量。出现下面情况时候,你的选择包括:

1.声明一个抽象的val,并且希望子类实现它的时候必须使用lazy进行修饰或者使用早期定义。如果不这么做,在进行构造的过程中,变量在某些使用的地方将会显示为未初始化;
2.声明一个抽象的def,并且希望子类实现它的时候必须使用lazy进行修饰。如果不这么做,每次重新访问的时候将会被再次计算;
3.声明一个可能会抛出具体异常的lazy变量,并且希望子类重写他。如果不这么做,他将会...抛出异常。

在初始化lazy变量的时候抛出异常,将会导致在下一次使用的时候,右边的值将会重新计算。

请注意,使用lazy进行多次修饰val变量的时候,将会造成一个新的危险:当第一次使用这个val变量的时候,val变量内循环会导致栈溢出。

使用早期定义

abstract class A {
  val x1: String
  val x2: String = "mom"
  println("A: " + x1 + ", " + x2)
}
class B extends {
  val x1: String = "hello"
} with A {
  println("B: " + x1 + ", " + x2)
}
class C extends {
  override val x2: String = "dad"
} with B {
  println("B: " + x1 + ", " + x2)
}
object A extends App {
  new C
}

使用早期定义会使你的代码看起来有点呆,它们的局限性包括早期代码块中他们声明跟定义的地方和他们引用的地方,并且它们书写的方式不如lazy修饰的那么方便。

使用常量值定义

abstract class A {
  val x1: String
  val x2: String = "mom"
  println("A: " + x1 + ", " + x2)
}
class B extends A{
  val x1:String="hello"
  final val x3="goodbye"
  println("B: "+x1+", "+x2)
}
class C extends B{
  override val x2:String="dad"
  println("C: "+x1+", "+x2)
}
abstract class D{
  val c:C
  val x3=c.x3
  println("D: "+c+" but "+ x3)
}
class E extends D{
  val c=new C
  println(s"E: ${c.x1},${c.x2}, and $x3...")
}
object E extends App {
  new E
}

结果输出

D: null but goodbye
A: null, null
B: hello, null
C: hello, dad
E: hello,dad, and goodbye...

有时候,你所需要的只是一个从interface处得到一个编译期常量。
常量值比早期定义的普通变量值更具限制。
本文章翻译自 Why is my abstract or overridden val null?

你可能感兴趣的:(初始化顺序2)