Skip to content

Commit cf9e29c

Browse files
committed
Update FAQ on initialization order
1 parent f767949 commit cf9e29c

File tree

1 file changed

+72
-76
lines changed

1 file changed

+72
-76
lines changed

_overviews/FAQ/initialization-order.md

+72-76
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ permalink: /tutorials/FAQ/:title.html
77

88
## Example
99

10-
To understand the problem, let's pick the following concrete example.
10+
The following example illustrates the problem:
1111

1212
abstract class A {
1313
val x1: String
@@ -26,7 +26,7 @@ To understand the problem, let's pick the following concrete example.
2626
println("C: " + x1 + ", " + x2)
2727
}
2828

29-
Let's observe the initialization order through the Scala REPL:
29+
In the Scala REPL we observe:
3030

3131
scala> new C
3232
A: null, null
@@ -36,129 +36,125 @@ Let's observe the initialization order through the Scala REPL:
3636
Only when we get to the constructor of `C` are both `x1` and `x2` initialized. Therefore, constructors of `A` and `B` risk running into `NullPointerException`s.
3737

3838
## Explanation
39-
A 'strict' or 'eager' val is one which is not marked lazy.
4039

41-
In the absence of "early definitions" (see below), initialization of strict vals is done in the following order.
40+
A "strict" or "eager" val is one which is not marked lazy.
41+
Initialization of strict vals is done in the following order:
4242

4343
1. Superclasses are fully initialized before subclasses.
4444
2. Otherwise, in declaration order.
4545

46-
Naturally when a val is overridden, it is not initialized more than once. So though x2 in the above example is seemingly defined at every point, this is not the case: an overridden val will appear to be null during the construction of superclasses, as will an abstract val.
46+
When a `val` is overridden, in fact its accessor method (the "getter") is overridden.
47+
So the access to `x2` in class `A` in fact invokes the overridden getter in class `C` which reads the underlying field `C.x2`.
48+
This field is not yet initialized during the construction of `A`.
4749

48-
There is a compiler flag which can be useful for identifying this situation:
50+
## Mitigation
4951

50-
**-Xcheckinit**: Add runtime check to field accessors.
52+
The [`-Ysafe-init` compiler flag](https://docs.scala-lang.org/scala3/reference/other-new-features/safe-initialization.html) in Scala 3 enables compiler warnings for accesses to uninitialized fields:
5153

52-
It is inadvisable to use this flag outside of testing. It adds significantly to the code size by putting a wrapper around all potentially uninitialized field accesses: the wrapper will throw an exception rather than allow a null (or 0/false in the case of primitive types) to silently appear. Note also that this adds a *runtime* check: it can only tell you anything about code paths which you exercise with it in place.
54+
-- Warning: Test.scala:8:6 ------------------
55+
8 | val x1: String = "hello"
56+
| ^
57+
| Access non-initialized value x1. Calling trace:
58+
| ├── class B extends A { [ Test.scala:7 ]
59+
| │ ^
60+
| ├── abstract class A { [ Test.scala:1 ]
61+
| │ ^
62+
| └── println("A: " + x1 + ", " + x2) [ Test.scala:5 ]
63+
| ^^
5364

54-
Using it on the opening example:
65+
In Scala 2, the `-Xcheckinit` flag adds runtime checks in the generated bytecode to identify accesses of uninitialized fields.
66+
The code then throws an exception rather than allowing a `null` (or `0` / `false` in the case of primitive types) to silently appear.
67+
Note that these runtime checks only test code that is actually exectued at runtime.
68+
The flag can be helpful to find accesses to uninitialized fields, but it should never be used in production due to its performance overhead.
5569

56-
% scalac -Xcheckinit a.scala
57-
% scala -e 'new C'
58-
scala.UninitializedFieldError: Uninitialized field: a.scala: 13
59-
at C.x2(a.scala:13)
60-
at A.<init>(a.scala:5)
61-
at B.<init>(a.scala:7)
62-
at C.<init>(a.scala:12)
63-
64-
### Solutions ###
70+
## Solutions
6571

6672
Approaches for avoiding null values include:
6773

68-
#### Use lazy vals ####
69-
70-
abstract class A {
71-
val x1: String
72-
lazy val x2: String = "mom"
74+
### Use class / trait parameters
7375

76+
abstract class A(val x1: String, val x2: String = "mom") {
7477
println("A: " + x1 + ", " + x2)
7578
}
76-
class B extends A {
77-
lazy val x1: String = "hello"
78-
79+
class B(x1: String = "hello", x2: String = "mom") extends A(x1, x2) {
7980
println("B: " + x1 + ", " + x2)
8081
}
81-
class C extends B {
82-
override lazy val x2: String = "dad"
83-
82+
class C(x2: String = "dad") extends B(x2 = x2) {
8483
println("C: " + x1 + ", " + x2)
8584
}
8685
// scala> new C
8786
// A: hello, dad
8887
// B: hello, dad
8988
// C: hello, dad
9089

91-
Usually the best answer. Unfortunately you cannot declare an abstract lazy val. If that is what you're after, your options include:
90+
Values passed as parameters to the superclass constructor are available in its body.
9291

93-
1. Declare an abstract strict val, and hope subclasses will implement it as a lazy val or with an early definition. If they do not, it will appear to be uninitialized at some points during construction.
94-
2. Declare an abstract def, and hope subclasses will implement it as a lazy val. If they do not, it will be re-evaluated on every access.
95-
3. Declare a concrete lazy val which throws an exception, and hope subclasses override it. If they do not, it will... throw an exception.
92+
Scala 3 also [supports trait parameters](https://docs.scala-lang.org/scala3/reference/other-new-features/trait-parameters.html).
9693

97-
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.
94+
Note that overriding a `val` class parameter is deprecated / disallowed in Scala 3.
95+
Doing so in Scala 2 can lead to surprising behavior.
9896

99-
Note that using multiple lazy vals creates a new risk: cycles among lazy vals can result in a stack overflow on first access.
97+
### Use lazy vals
10098

101-
#### Use early definitions ####
10299
abstract class A {
103-
val x1: String
104-
val x2: String = "mom"
100+
lazy val x1: String
101+
lazy val x2: String = "mom"
105102

106103
println("A: " + x1 + ", " + x2)
107104
}
108-
class B extends {
109-
val x1: String = "hello"
110-
} with A {
105+
class B extends A {
106+
lazy val x1: String = "hello"
107+
111108
println("B: " + x1 + ", " + x2)
112109
}
113-
class C extends {
114-
override val x2: String = "dad"
115-
} with B {
110+
class C extends B {
111+
override lazy val x2: String = "dad"
112+
116113
println("C: " + x1 + ", " + x2)
117114
}
118115
// scala> new C
119116
// A: hello, dad
120117
// B: hello, dad
121118
// C: hello, dad
122119

123-
Early definitions are a bit unwieldy, there are limitations as to what can appear and what can be referenced in an early definitions block, and they don't compose as well as lazy vals: but if a lazy val is undesirable, they present another option. They are specified in SLS 5.1.6.
120+
Note that abstract `lazy val`s are supported in Scala 3, but not in Scala 2.
121+
In Scala 2, you can define an abstract `val` or `def` instead.
124122

125-
Note that early definitions are deprecated in Scala 2.13; they will be replaced by trait parameters in Scala 3. So, early definitions are not recommended for use if future compatibility is a concern.
123+
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.
126124

127-
#### Use constant value definitions ####
128-
abstract class A {
129-
val x1: String
130-
val x2: String = "mom"
125+
Note that using multiple lazy vals creates a new risk: cycles among lazy vals can result in a stack overflow on first access.
131126

132-
println("A: " + x1 + ", " + x2)
133-
}
134-
class B extends A {
135-
val x1: String = "hello"
136-
final val x3 = "goodbye"
127+
### Use a nested object
137128

138-
println("B: " + x1 + ", " + x2)
139-
}
140-
class C extends B {
141-
override val x2: String = "dad"
129+
Sometimes, uninitialized state in a subclass is accessed during construction of a superclass:
142130

143-
println("C: " + x1 + ", " + x2)
131+
class Adder {
132+
var sum = 0
133+
def add(x: Int): Unit = sum += x
134+
add(1)
144135
}
145-
abstract class D {
146-
val c: C
147-
val x3 = c.x3 // no exceptions!
148-
println("D: " + c + " but " + x3)
136+
class LogAdder extends Adder {
137+
private var added: Set[Int] = Set.empty
138+
override def add(x: Int): Unit = { added += x; super.add(x) }
149139
}
150-
class E extends D {
151-
val c = new C
152-
println(s"E: ${c.x1}, ${c.x2}, and $x3...")
140+
141+
In this case the state can be initialized on demand by wrapping it into a local object:
142+
143+
class Adder {
144+
var sum = 0
145+
def add(x: Int): Unit = sum += x
146+
add(1)
153147
}
154-
//scala> new E
155-
//D: null but goodbye
156-
//A: null, null
157-
//B: hello, null
158-
//C: hello, dad
159-
//E: hello, dad, and goodbye...
148+
class LogAdder extends Adder {
149+
private object state {
150+
var added: Set[Int] = Set.empty
151+
}
152+
import state._
153+
override def add(x: Int): Unit = { added += x; super.add(x) }
154+
}
155+
156+
### Early definitions: deprecated
160157

161-
Sometimes all you need from an interface is a compile-time constant.
158+
Scala 2 supports early definitinos, but they are deprecated in Scala 2.13 and unsupported in Scala 3.
159+
See the [migration guide](https://docs.scala-lang.org/scala3/guides/migration/incompat-dropped-features.html#early-initializer) for more information.
162160

163-
Constant values are stricter than strict and earlier than early definitions and have even more limitations,
164-
as they must be constants. They are specified in SLS 4.1.

0 commit comments

Comments
 (0)