You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
To understand the problem, let's pick the following concrete example.
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.
11
14
12
15
abstract class A {
13
16
val x1: String
14
17
val x2: String = "mom"
15
18
16
-
println("A: " + x1 + ", " + x2)
19
+
println(s"A: $x1, $x2")
17
20
}
18
21
class B extends A {
19
22
val x1: String = "hello"
20
23
21
-
println("B: " + x1 + ", " + x2)
24
+
println(s"B: $x1, $x2")
22
25
}
23
26
class C extends B {
24
27
override val x2: String = "dad"
25
28
26
-
println("C: " + x1 + ", " + x2)
29
+
println(s"C: $x1, $x2")
27
30
}
28
31
29
-
Let's observe the initialization order through the Scala REPL:
32
+
In the Scala REPL we observe:
30
33
31
34
scala> new C
32
35
A: null, null
33
36
B: hello, null
34
37
C: hello, dad
35
38
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.
37
42
38
43
## Explanation
39
-
A 'strict' or 'eager' val is one which is not marked lazy.
40
44
41
-
In the absence of "early definitions" (see below), initialization of strict vals is done in the following order.
45
+
A "strict" or "eager" val is a `val` which is not a `lazy val`.
46
+
Initialization of strict vals is done in the following order:
42
47
43
48
1. Superclasses are fully initialized before subclasses.
44
-
2. Otherwise, in declaration order.
45
-
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.
47
-
48
-
There is a compiler flag which can be useful for identifying this situation:
49
-
50
-
**-Xcheckinit**: Add runtime check to field accessors.
51
-
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.
Usually the best answer. Unfortunately you cannot declare an abstract lazy val. If that is what you're after, your options include:
100
+
Values passed as parameters to the superclass constructor are available in its body.
92
101
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.
102
+
Scala 3 also [supports trait parameters](https://docs.scala-lang.org/scala3/reference/other-new-features/trait-parameters.html).
96
103
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.
104
+
Note that overriding a `val` class parameter is deprecated / disallowed in Scala 3.
105
+
Doing so in Scala 2 can lead to surprising behavior.
98
106
99
-
Note that using multiple lazy vals creates a new risk: cycles among lazy vals can result in a stack overflow on first access.
107
+
### Use lazy vals
100
108
101
-
#### Use early definitions ####
102
109
abstract class A {
103
-
val x1: String
104
-
val x2: String = "mom"
110
+
lazy val x1: String
111
+
lazy val x2: String = "mom"
105
112
106
113
println("A: " + x1 + ", " + x2)
107
114
}
108
-
class B extends {
109
-
val x1: String = "hello"
110
-
} with A {
115
+
class B extends A {
116
+
lazy val x1: String = "hello"
117
+
111
118
println("B: " + x1 + ", " + x2)
112
119
}
113
-
class C extends {
114
-
override val x2: String = "dad"
115
-
} with B {
120
+
class C extends B {
121
+
override lazy val x2: String = "dad"
122
+
116
123
println("C: " + x1 + ", " + x2)
117
124
}
118
125
// scala> new C
119
126
// A: hello, dad
120
127
// B: hello, dad
121
128
// C: hello, dad
122
129
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.
130
+
Note that abstract `lazy val`s are supported in Scala 3, but not in Scala 2.
131
+
In Scala 2, you can define an abstract `val` or `def` instead.
124
132
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.
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.
126
134
127
-
#### Use constant value definitions ####
128
-
abstract class A {
129
-
val x1: String
130
-
val x2: String = "mom"
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.
131
137
132
-
println("A: " + x1 + ", " + x2)
133
-
}
134
-
class B extends A {
135
-
val x1: String = "hello"
136
-
final val x3 = "goodbye"
138
+
### Use a nested object
137
139
138
-
println("B: " + x1 + ", " + x2)
139
-
}
140
-
class C extends B {
141
-
override val x2: String = "dad"
140
+
For purposes of initialization, an object that is not top-level is the same as a lazy val.
142
141
143
-
println("C: " + x1 + ", " + x2)
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:
146
+
147
+
class Adder {
148
+
var sum = 0
149
+
def add(x: Int): Unit = sum += x
150
+
add(1) // in LogAdder, the `added` set is not initialized yet
0 commit comments