为什么使用val实现抽象方法并在val表达式中从超类进行调用返回NullPointerException

时间:2018-07-19 15:04:06

标签: scala object-initialization

我有一个抽象类,其中有一个未实现的方法numbers,该方法返回一个数字列表,该方法用于另一个val属性初始化:

abstract class Foo {
  val calcNumbers = numbers.map(calc)
  def numbers: List[Double]
}

实现类使用val表达式实现:

class MainFoo extends Foo {
  val numbers = List(1,2,3)
}

这可以很好地编译,但是在运行时会引发NullPointerException并指向val calcNumbers行:

[error] (run-main-0) java.lang.ExceptionInInitializerError
[error] java.lang.ExceptionInInitializerError
...
[error] Caused by: java.lang.NullPointerException
...

但是,当我将实现的方法更改为def时,它会起作用:

def numbers = List(1,2,3)

那是为什么?它与初始化顺序有关吗?由于没有编译时错误/警告,将来如何避免这种情况? Scala如何允许这种不安全的操作?

1 个答案:

答案 0 :(得分:3)

这是代码初始化MainFoo时试图执行的操作:

  1. 分配一块内存,其空间足以容纳val calcNumbersval numbers,最初设置为0
  2. 运行基类Foo的初始化程序,在初始化numbers.map的同时尝试调用calcNumbers
  3. 运行子类MainFoo的初始化程序,在此它将numbers初始化为List(1, 2, 3)

由于numbers尚未初始化,当您尝试在val calcNumbers = ...中访问它时,会得到NullPointerException

可能的解决方法:

  1. numbers中将MainFoo设为def
  2. numbers中将MainFoo设为lazy val
  3. calcNumbers中将Foo设为def
  4. calcNumbers中将Foo设为lazy val

每种解决方法都可以防止对未初始化的值numbers.map进行热切的值初始化调用numbers

FAQ提供了其他一些解决方案,并且还提到了(昂贵的)编译器标志-Xcheckinit


您可能还会发现这些相关的答案很有用:

  1. Scala overridden value: parent code is run but value is not assigned at parent

  2. Assertion with require in abstract superclass creates NPE