我今天在处理更多面向对象的部分时遇到了一些NPE - 我不确定为什么但是重写字段的构造函数上有空值。
有什么方法可以解决这个问题吗?这是一个演示行为的示例。你会注意到基类' println导致打印null。如果基类尝试使用' line'在基类的构造函数中,它将触发一个npe。
scala> class Base {val line = "hey1"; println(line)}
defined class Base
scala> class Extended extends Base{ override val line = "hey2"; println(line)}
defined class Extended
scala> new Extended
null
hey2
res0: Extended = Extended@55991e21
scala> new Base
hey1
res1: Base = Base@1cc21a68
例如,这是一个演示空指针异常的示例。
scala> class Base { val line = "hello"; println(line.reverse)}
defined class Base
^
scala> class Extend extends Base { override val line ="exthello"; println(line.reverse);}
defined class Extend
scala> new Extend
java.lang.NullPointerException
答案 0 :(得分:3)
来源:https://github.com/paulp/scala-faq/wiki/Initialization-Order
(感谢Ghik - 赞成评论或接受的答案而不是这个答案 - 这是为了完整性和历史记录而复制的。)
初始化订单 为什么我的抽象或覆盖的val为null?
请考虑以下事项。
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)
}
// scala> new C
// A: null, null
// B: hello, null
// C: hello, dad
A'严格'或者渴望'渴望' val是一个没有标记为懒惰的。
缺少"早期定义" (见下文),严格的val的初始化按以下顺序完成。
超类在子类之前完全初始化。 否则,按声明顺序。 当val被覆盖时,它自然不会被初始化多次。因此,虽然上面示例中的x2似乎在每个点上定义,但情况并非如此:在构造超类时,重写的val似乎为null,而抽象的val也是如此。
有一个编译器标志可用于识别这种情况:
-Xcheckinit:将运行时检查添加到字段访问者。
不建议在测试之外使用此标志。它通过在所有可能未初始化的字段访问周围放置一个包装器来显着增加代码大小:包装器将抛出异常而不是允许空(或在原始类型的情况下为0 / false)静默显示。另请注意,这会添加运行时检查:它只能告诉您有关使用它的代码路径的任何信息。
在开场示例中使用它:
% scalac -Xcheckinit a.scala
% scala -e 'new C'
scala.UninitializedFieldError: Uninitialized field: a.scala: 13
at C.x2(a.scala:13)
at A.<init>(a.scala:5)
at B.<init>(a.scala:7)
at C.<init>(a.scala:12)
避免空值的方法包括:
使用延迟值。
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)
}
// scala> new C
// A: hello, dad
// B: hello, dad
// C: hello, dad
通常是最好的答案。不幸的是,你不能宣布一个抽象的懒惰的val。如果这就是你所追求的,你的选择包括:
声明一个抽象严格的val,希望子类将它实现为一个惰性val或早期定义。如果他们不这样做,在施工期间的某些时候似乎没有初始化。 声明一个抽象def,希望子类将它实现为一个懒惰的val。如果他们不这样做,将在每次访问时重新评估。 声明一个抛出异常的具体lazy val,希望子类覆盖它。如果他们不这样做,它会...抛出异常。 初始化延迟val期间的异常将导致在下次访问时重新评估右侧:请参阅SLS 5.2。
请注意,使用多个延迟值会产生新的风险:延迟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("C: " + x1 + ", " + x2)
}
// scala> new C
// A: hello, dad
// B: hello, dad
// C: hello, dad
早期定义有点笨拙,在早期定义块中可以出现的内容和可以引用的内容存在限制,并且它们不构成和懒惰的val:但是如果懒惰的val是不合需要的,他们提出另一种选择。它们在SLS 5.1.6中指定。
使用常量值定义。
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 // no exceptions!
println("D: " + c + " but " + x3)
}
class E extends D {
val c = new C
println(s"E: ${c.x1}, ${c.x2}, and $x3...")
}
//scala> new E
//D: null but goodbye
//A: null, null
//B: hello, null
//C: hello, dad
//E: hello, dad, and goodbye...
有时,您只需要一个接口就可以获得编译时常量。 常量值比严格和早于定义更严格,并且具有更多限制,因为它们必须是常量。它们在SLS 4.1中指定。
答案 1 :(得分:2)
如果你想保留你的结构,而不是使用def或lazy val而不是val:
class Base {def line = "hey1"; println(line)}
class Extend extends Base { override def line ="exthello"; println(line);}
但更好的覆盖参数列表:
scala> class Base(val a: String = "hey1") {println(a)}
defined class Base
scala> new Base
hey1
scala> class Extend(override val a:String = "hey2") extends Base(a) {println(a)}
defined class Extend
scala> new Extend
hey2
hey2
res18: Extend = Extend@4d7efd4