While testing Scala code, I've run into a strange NPE while asserting on a value from an object.
Here is the minimal code to reproduce the issue:
main/scala/Playground.scala:
object Playground extends App {
val greeting = "Hello Scala"
println(greeting)
}
test/scala/PlaygroundSpec.scala:
import org.scalatest.wordspec._
class PlaygroundSpec extends AnyWordSpec {
"The playground code" should {
"say hello" in {
assert(Playground.greeting.contains("Hello")) // Throws NPE because greeting is null. How???
}
}
}
The sample program runs just fine and prints "Hello Scala", but the test throws a NullPointerException
on the assertion line, because greeting
is null
.
How could greeting
be null
if it is initialized with a string constant?
Note: Adding
lazy
to theval
declaration makes it work and the test passes.
In Scala 2 App
extends DelayedInit
, so compiler magically rewrites initialisation code such that the initialisation of fields is moved to delayedInit
method, for example,
object Playground extends App {
val greeting = "Hello Scala"
println(greeting)
}
becomes something like
object Playground extends App {
private var greeting: String = null
def greeting(): String = greeting
def delayedInit(): Unit = {
greeting = "Hello Scala"
println(greeting())
}
def main(args: Array[String]) = {
// indirectly call delayedInit
...
}
}
Now we can see
assert(Playground.greeting.contains("Hello"))
becomes
assert(null.contains("Hello"))
as delayedInit
method did not get called. To prove the point observe how the following works
Playground.main(Array.empty) // delayedInit gets indirectly called
assert(Playground.greeting.contains("Hello")) // ok
Adding lazy to the val declaration makes it work and the test passes.
This works because lazy val greeting
effectively turns the field into a method which moves it out of the initialisation code so it does not become part of delayedInit
.
Clearly this is confusing so Scala 3 Dropped: Delayedinit.