Skip to content

Commit 19967a9

Browse files
committed
More words for initialization FAQ
1 parent 0d68382 commit 19967a9

File tree

1 file changed

+44
-24
lines changed

1 file changed

+44
-24
lines changed

_overviews/FAQ/initialization-order.md

+44-24
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,26 @@ permalink: /tutorials/FAQ/:title.html
77

88
## Example
99

10-
The following example illustrates the problem:
10+
The following example illustrates how classes in a subclass relation
11+
witness the initialization of two fields which are inherited from
12+
their top-most parent. The values are printed during the constructor
13+
of each class, that is, when an instance is initialized.
1114

1215
abstract class A {
1316
val x1: String
1417
val x2: String = "mom"
1518

16-
println("A: " + x1 + ", " + x2)
19+
println(s"A: $x1, $x2")
1720
}
1821
class B extends A {
1922
val x1: String = "hello"
2023

21-
println("B: " + x1 + ", " + x2)
24+
println(s"B: $x1, $x2")
2225
}
2326
class C extends B {
2427
override val x2: String = "dad"
2528

26-
println("C: " + x1 + ", " + x2)
29+
println(s"C: $x1, $x2")
2730
}
2831

2932
In the Scala REPL we observe:
@@ -33,39 +36,46 @@ In the Scala REPL we observe:
3336
B: hello, null
3437
C: hello, dad
3538

36-
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.
39+
Only when we get to the constructor of `C` are both `x1` and `x2` properly initialized.
40+
Therefore, constructors of `A` and `B` risk running into `NullPointerException`s,
41+
since fields are null-valued until set by a constructor.
3742

3843
## Explanation
3944

40-
A "strict" or "eager" val is one which is not marked lazy.
45+
A "strict" or "eager" val is a `val` which is not a `lazy val`.
4146
Initialization of strict vals is done in the following order:
4247

4348
1. Superclasses are fully initialized before subclasses.
44-
2. Otherwise, in declaration order.
49+
2. Within the body or "template" of a class, vals are initialized in declaration order,
50+
the order in which they are written in source.
4551

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`.
52+
When a `val` is overridden, it's more precise to say that its accessor method (the "getter") is overridden.
53+
So the access to `x2` in class `A` invokes the overridden getter in class `C`.
54+
That getter reads the underlying field `C.x2`.
4855
This field is not yet initialized during the construction of `A`.
4956

5057
## Mitigation
5158

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:
59+
The [`-Wsafe-init` compiler flag](https://docs.scala-lang.org/scala3/reference/other-new-features/safe-initialization.html)
60+
in Scala 3 enables a compile-time warning for accesses to uninitialized fields:
5361

54-
-- Warning: Test.scala:8:6 ------------------
62+
-- Warning: Test.scala:8:6 -----------------------------------------------------
5563
8 | val x1: String = "hello"
5664
| ^
5765
| Access non-initialized value x1. Calling trace:
58-
| ├── class B extends A { [ Test.scala:7 ]
66+
| ├── class B extends A { [ Test.scala:7 ]
5967
| │ ^
60-
| ├── abstract class A { [ Test.scala:1 ]
68+
| ├── abstract class A { [ Test.scala:1 ]
6169
| │ ^
62-
| └── println("A: " + x1 + ", " + x2) [ Test.scala:5 ]
63-
| ^^
70+
| └── println(s"A: $x1, $x2") [ Test.scala:5 ]
71+
| ^^
6472

6573
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.
74+
That code throws an exception when an uninitialized field is referenced
75+
that would otherwise be used as a `null` value (or `0` or `false` in the case of primitive types).
76+
Note that these runtime checks only report code that is actually executed at runtime.
77+
Although these checks can be helpful to find accesses to uninitialized fields during development,
78+
it is never advisable to enable them in production code due to the performance cost.
6979

7080
## Solutions
7181

@@ -120,25 +130,31 @@ Doing so in Scala 2 can lead to surprising behavior.
120130
Note that abstract `lazy val`s are supported in Scala 3, but not in Scala 2.
121131
In Scala 2, you can define an abstract `val` or `def` instead.
122132

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.
133+
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.
124134

125-
Note that using multiple lazy vals creates a new risk: cycles among lazy vals can result in a stack overflow on first access.
135+
Note that using multiple lazy vals incurs a new risk: cycles among lazy vals can result in a stack overflow on first access.
136+
When lazy vals are annotated as thread-safe in Scala 3, they risk deadlock.
126137

127138
### Use a nested object
128139

129-
Sometimes, uninitialized state in a subclass is accessed during construction of a superclass:
140+
For purposes of initialization, an object that is not top-level is the same as a lazy val.
141+
142+
There may be reasons to prefer a lazy val, for example to specify the type of an implicit value,
143+
or an object where it is a companion to a class. Otherwise, the most convenient syntax may be preferred.
144+
145+
As an example, uninitialized state in a subclass may be accessed during construction of a superclass:
130146

131147
class Adder {
132148
var sum = 0
133149
def add(x: Int): Unit = sum += x
134-
add(1)
150+
add(1) // in LogAdder, the `added` set is not initialized yet
135151
}
136152
class LogAdder extends Adder {
137153
private var added: Set[Int] = Set.empty
138154
override def add(x: Int): Unit = { added += x; super.add(x) }
139155
}
140156

141-
In this case the state can be initialized on demand by wrapping it into a local object:
157+
In this case, the state can be initialized on demand by wrapping it in a local object:
142158

143159
class Adder {
144160
var sum = 0
@@ -155,6 +171,10 @@ In this case the state can be initialized on demand by wrapping it into a local
155171

156172
### Early definitions: deprecated
157173

158-
Scala 2 supports early definitinos, but they are deprecated in Scala 2.13 and unsupported in Scala 3.
174+
Scala 2 supports early definitions, but they are deprecated in Scala 2.13 and unsupported in Scala 3.
159175
See the [migration guide](https://docs.scala-lang.org/scala3/guides/migration/incompat-dropped-features.html#early-initializer) for more information.
160176

177+
Constant value definitions (specified in SLS 4.1 and available in Scala 2)
178+
and inlined definitions (in Scala 3) can work around initialization order issues
179+
because they can supply constant values without evaluating an instance that is not yet initialized.
180+

0 commit comments

Comments
 (0)