Example
The following example illustrates how classes in a subclass relation witness the initialization of two fields which are inherited from their top-most parent. The values are printed during the constructor of each class, that is, when an instance is initialized.
abstract class A {
val x1: String
val x2: String = "mom"
println(s"A: $x1, $x2")
}
class B extends A {
val x1: String = "hello"
println(s"B: $x1, $x2")
}
class C extends B {
override val x2: String = "dad"
println(s"C: $x1, $x2")
}
In the Scala REPL we observe:
scala> new C
A: null, null
B: hello, null
C: hello, dad
Only when we get to the constructor of C
are both x1
and x2
properly initialized.
Therefore, constructors of A
and B
risk running into NullPointerException
s,
since fields are null-valued until set by a constructor.
Explanation
A “strict” or “eager” val is a val
which is not a lazy val
.
Initialization of strict vals is done in the following order:
- Superclasses are fully initialized before subclasses.
- Within the body or “template” of a class, vals are initialized in declaration order, the order in which they are written in source.
When a val
is overridden, it’s more precise to say that its accessor method (the “getter”) is overridden.
So the access to x2
in class A
invokes the overridden getter in class C
.
That getter reads the underlying field C.x2
.
This field is not yet initialized during the construction of A
.
Mitigation
The -Wsafe-init
compiler flag
in Scala 3 enables a compile-time warning for accesses to uninitialized fields:
-- Warning: Test.scala:8:6 -----------------------------------------------------
8 | val x1: String = "hello"
| ^
| Access non-initialized value x1. Calling trace:
| ├── class B extends A { [ Test.scala:7 ]
| │ ^
| ├── abstract class A { [ Test.scala:1 ]
| │ ^
| └── println(s"A: $x1, $x2") [ Test.scala:5 ]
| ^^
In Scala 2, the -Xcheckinit
flag adds runtime checks in the generated bytecode to identify accesses of uninitialized fields.
That code throws an exception when an uninitialized field is referenced
that would otherwise be used as a null
value (or 0
or false
in the case of primitive types).
Note that these runtime checks only report code that is actually executed at runtime.
Although these checks can be helpful to find accesses to uninitialized fields during development,
it is never advisable to enable them in production code due to the performance cost.
Solutions
Approaches for avoiding null values include:
Use class / trait parameters
abstract class A(val x1: String, val x2: String = "mom") {
println("A: " + x1 + ", " + x2)
}
class B(x1: String = "hello", x2: String = "mom") extends A(x1, x2) {
println("B: " + x1 + ", " + x2)
}
class C(x2: String = "dad") extends B(x2 = x2) {
println("C: " + x1 + ", " + x2)
}
// scala> new C
// A: hello, dad
// B: hello, dad
// C: hello, dad
Values passed as parameters to the superclass constructor are available in its body.
Scala 3 also supports trait parameters.
Note that overriding a val
class parameter is deprecated / disallowed in Scala 3.
Doing so in Scala 2 can lead to surprising behavior.
Use lazy vals
abstract class A {
lazy 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
Note that abstract lazy val
s are supported in Scala 3, but not in Scala 2.
In Scala 2, you can define an abstract val
or def
instead.
An exception during initialization of a lazy val will cause the right-hand side to be re-evaluated on the next access; see SLS 5.2.
Note that using multiple lazy vals incurs a new risk: cycles among lazy vals can result in a stack overflow on first access. When lazy vals are annotated as thread-safe in Scala 3, they risk deadlock.
Use a nested object
For purposes of initialization, an object that is not top-level is the same as a lazy val.
There may be reasons to prefer a lazy val, for example to specify the type of an implicit value, or an object where it is a companion to a class. Otherwise, the most convenient syntax may be preferred.
As an example, uninitialized state in a subclass may be accessed during construction of a superclass:
class Adder {
var sum = 0
def add(x: Int): Unit = sum += x
add(1) // in LogAdder, the `added` set is not initialized yet
}
class LogAdder extends Adder {
private var added: Set[Int] = Set.empty
override def add(x: Int): Unit = { added += x; super.add(x) }
}
In this case, the state can be initialized on demand by wrapping it in a local object:
class Adder {
var sum = 0
def add(x: Int): Unit = sum += x
add(1)
}
class LogAdder extends Adder {
private object state {
var added: Set[Int] = Set.empty
}
import state._
override def add(x: Int): Unit = { added += x; super.add(x) }
}
Early definitions: deprecated
Scala 2 supports early definitions, but they are deprecated in Scala 2.13 and unsupported in Scala 3. See the migration guide for more information.
Constant value definitions (specified in SLS 4.1 and available in Scala 2) and inlined definitions (in Scala 3) can work around initialization order issues because they can supply constant values without evaluating an instance that is not yet initialized.