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

Scala 为什么在val表达式中使用val实现抽象方法并从超类调用返回NullPointerException,scala,object-initialization,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) } 这可以很好地编译,但在运行时它抛出一个NullPointer

我有一个抽象类,它有一个未实现的方法
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是如何允许这种不安全的操作的?

下面是您的代码在初始化
MainFoo
时尝试执行的操作:

  • val calcNumbers
    val numbers
    分配一块内存,初始设置为
    0
  • 运行基类
    Foo
    的初始值设定项,在初始化
    calcNumbers
    时尝试调用
    numbers.map
  • 运行子类
    MainFoo
    的初始值设定项,将
    numbers
    初始化为
    列表(1、2、3)
  • 由于在
    val calcNumbers=…
    中尝试访问时,
    numbers
    尚未初始化,因此会出现
    NullPointerException

    可能的解决办法:

  • MainFoo
    a
    def
  • MainFoo
    a
    lazy val
  • Foo
    a
    def
  • Foo
    a
    lazy val
  • 每一种解决方法都可以防止一个急切值初始化调用未初始化值
    numbers.map

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


    您可能还会发现以下相关答案很有用:


  • number
    中缺少
    s
    ?可能是@HüseyinZengin的重复项,似乎有点太模糊了,不是吗?另一个非重复:(在这里,OP只是交换了
    val
    s的顺序,没有抽象类,没有def by val覆盖)。一个更好的复制品:,但是答案可以更详细。@Andreytukin你是对的,这个问题本身不是一个完全的复制品,但这个问题基本上是早期初始化的结果。答案完全覆盖了这个问题,谢谢你的解释。虽然当Scala允许这个漏洞/陷阱而没有默认警告时,我感到震惊…@texasbruce哦,对象初始化很难。它在具有继承、特征、异常、垃圾收集、类加载器、内置单例和多线程的语言中都能工作,这是一个奇迹……为了避免这个问题,应该禁止在Scala中使用
    val
    重写方法。仅允许
    def
    lazy val