I'm experiencing a bizarre bug. One of the case objects (only one) gets null
as its value and obviously causes Null pointer exceptions later in the life. But: why?
I've tried different things (renaming objects, changing hesitance, even rewriting the same line twice, getting one of them to compile nice, while another gives null
). I've cloned the repo to make sure it's not some funny transitive effect. Both Scala 2.11.4 and 2.11.5 cause the same.
The code is rather twisty and cutting it off its context makes little sense. But here it is, in case it rings any bells on someone else.
sealed trait A {}
sealed trait B extends A {}
sealed trait C { val n: Int }
object C {
val C1: C = new C { val n = 1 }
val C2: C = new C { val n = 2 }
val C3: C = new C { val n = 3 }
val C4a: C = new C { val n = 4 }
val C4b: C = new C { val n = 5 }
}
class D (depends: Seq[C], f: (Seq[Double]) => Double) {
// ...methods removed
}
sealed trait E { val e: Int = 1 }
object D {
import C._
private def f( v: Seq[Double] ): Double = 0
private def list( acc: Boolean ) = List(
C1, C2, C3,
if (acc) C4a else C4b
)
case object D1 extends D( list(true), f ) with E // <-- this gets to be 'null'!!
case object D2 extends D( list(false), f )
val keys: Seq[D] = List(D1, D2)
println(s"D1: $D1")
println(s"D2: $D2")
assert(D1 != null) // <-- caught here
assert(D2 != null)
}
I tried to keep as much of the details in the code, as possible, in case they matter somehow.
What can this be?
I tagged it as 'heisenbug' but when I compile stuff, it's consistent (i.e. successive test runs would always produce the same effect).
EDIT: I originally said this only happened with sbt test
(and not sbt run
) but that's not true. Both were equally affected.
EDIT: Seems @dk14 has filed this as Scala issue 9115. Thanks!
In object-oriented computer programming, a null object is an object with no referenced value or with defined neutral ("null") behavior. The null object design pattern describes the uses of such objects and their behavior (or lack thereof).
Typically, you'll check for null using the triple equality operator ( === or !== ), also known as the strict equality operator, to be sure that the value in question is definitely not null: object !== null . That code checks that the variable object does not have the value null .
I have no idea what exactly happens, but I suspect there is some kind of construction order involved. I can reproduce the problem 100% of the time, if I paste the code and do
scala> D.D1
D1: null
D2: D2
java.lang.AssertionError: assertion failed
at scala.Predef$.assert(Predef.scala:151)
... 47 elided
Also if I ask for D2
first, it will fail too, so it's not related to E
.
scala> D.D2
D1: D1
D2: null
java.lang.AssertionError: assertion failed
at scala.Predef$.assert(Predef.scala:151)
... 47 elided
But if I ask for D
first, it works 100% of the time:
scala> D
D1: D1
D2: D2
res1: D.type = D$@50434135
scala> D.D1
res2: D.D1.type = D1
I succeed to distill the problem into shorter code example:
class Bug(val f: Int) {}
object Bug {
private def f(n: Int): Int = n
case object Bug1 extends Bug(f(1))
case object Bug2 extends Bug(f(2))
val values: List[Bug] = List(Bug1, Bug2)
println(s"1: $Bug1")
println(s"2: $Bug2")
}
Asking for Bug.Bug1
will fail. Yet if I remove println
lines - everything works. Or "execute" Bug
first. Yet having values
is ok.
I guess some initialisation locks go wrong way, when asking directly for Bug.Bug1
or Bug.Bug2
.
It's not working only when you directly access D.D1
or D.D2
before D
object itself as it's not initialized here, so case object
values are still null
's. Seems like a bug for me (should initialize D
before calling D.D1
). Workaround: just call D
itself (to run initialization) somewhere before D.D1
. See, https://stackoverflow.com/a/26072435/1809978 - lazy initialization is actually a feature, but not in the case you've found. Simplified example:
object D {
def aaa = 1 //that’s the reason
class Z (depends: Any)
case object D1 extends Z(aaa) // <-- this gets to be 'null'!!
case object D2 extends Z(aaa)
println(D1)
println(D2)
}
Results :
defined object D
scala> D.D1
null
D2
res32: D.D1.type = D1
After re-definition of D
:
defined object D
scala> D.D2
D1
null
res34: D.D2.type = D2
So scala forgots to initialize the requested sub-object (if it refers to some another member of object inside sub-object's definition) before running enclosing object initialization. It initializes this sub-object after (when aaa is defined). I think primary intention was to initialize all referred members before requested member, but it's still seems a bug as it changes intialization order, depending on how D was used in the code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With