Extend member inside object may be null in some condiction

Code

object Test extends App {

  sealed abstract class Op(val op: String) {
    override def toString: String = s"${op}"
  }

  object Op {

    val prefix = ""

    object And extends Op(s"${prefix}and")

    object Or extends Op(s"${prefix}or")


    val list = List(And, Or)

  }

  println(Op.Or)

  println(Op.And)

  println(Op.list)

  println(List(Op.Or, Op.And))

}

output is

or
and
List(and, null)
List(or, and)

I have read docs here
https://docs.scala-lang.org/tutorials/FAQ/initialization-order.html

But I dont know how null generate here, can someone explain it ? Thanks.


More info:

  1. remove prefix, it works well
  2. change the order of println(Op.Or) and println(Op.And), the null value will change
  3. change order of println(Op.list) at first, it works well
  println(Op.list)

  println(Op.Or)

  println(Op.And)

// output
List(and, or)
or
and
List(or, and)

Thank you for posting this example. In the past I often encountered this problem, and always assumed it was due to the way objects are created in Scala (which is lazy). This is especially problematic in a multithreaded application where you have no control over the order in which the inner objects are called. My solution was to call the all outer objects at least once before starting any threads. That approach also works in your example.

However, you show that the use of prefix also has an impact on the behaviour. So there may be actually more to it than meets the eye. I have no idea what. Can this be a bug? If not, i think this non predictive behaviour is unwanted to say the least.

1 Like

My first idea was also to “force” initialization of the enclosing object.

To answer your second question about why prefix matters: that forces init of Op (and assigns Op.list) before Or has completed and assigned its module field.

Moving prefix outside Op also solves that issue.

After a some simplification and adding some tracing with the code i found the following. Take the code

class Op(val op: String) :
  println(s"  in class '$op'")
  override def toString: String = s"${op}"

object Op :
  println("  enter object")
  val prefix = ""
  object And extends Op(s"${prefix}and") { println("  created 'And'") }
  object Or extends Op(s"${prefix}or")   { println("  created 'Or'")  }
  val list = List(And, Or)
  println("  exit object")
  
println("test Op.Or => ")
println("=> "+Op.Or)
println("test Op.And => ")
println("=> "+Op.And)
println("test Op.List => ")
println("=> "+Op.list)
println("test List(Op.Or, Op.And) => ")
println("=> "+List(Op.Or, Op.And))

you get

test Op.Or => 
  enter object
  in class 'and'
  created 'And'
  exit object
  in class 'or'
  created 'Or'
=> or
test Op.And => 
=> and
test Op.List => 
=> List(and, null)
test List(Op.Or, Op.And) => 
=> List(or, and)

But if you remove the call to prefix:

  object And extends Op("and") { println("  created 'And'") }
  object Or extends Op("or")   { println("  created 'Or'")  }

you get:

test Op.Or => 
  in class 'or'
  created 'Or'
=> or
test Op.And => 
  in class 'and'
  created 'And'
=> and
test Op.List => 
  enter object
  exit object
=> List(and, or)
test List(Op.Or, Op.And) => 
=> List(or, and)

The latter shows the strict nested lazy object construction, whereas the former needs something from the object Op, so it enters its creation, and thus also the object And before its actual call to Op.And, finishes all the steps of the creation, and makes use of the incomplete object Or in the definition of val list = List(And, Or). After that Or itself is completed, but the definition of list remains as is.

1 Like
lazy val list = List(And, Or)

is another way around the problem.

There are several tickets about the different flavors of init order problems that involve class initialization.

This is exactly this example:

nested object init order

but also this and others:

nested objects

Circular object dependency

Thank you for the helpful link!

Thanks for these references. Incredible that these serious issues are open for so long, especially given the efforts to reduce null pointer exceptions in other areas. The problem with these issues is that it can take a long time before running code gets to the point where it is a problem. It is even worse for multi threaded applications, where at first startup the code runs without any problem and than, at second startup, it crashes after a month or so (because the construction order is reversed, and since these objects are only constructed once, its like rolling the dice).

I have been hit on multiple occasions by such bugs in production code, so that now i force manual construction of all critical objects at startup by hand. This works, but is error prone of course, you easily forget an object when you add code. It would be better if the compiler handled this.

scala.reflect has a unit test that computes lazy values to force and emits the source code to do it. Automation reduces errors.

Complex static initialization has always been a hazard (that is, also in Java). I think a good reason to invoke “design patterns” in this case is to identify it as an anti-pattern and don’t do that.

Hopefully the “init checker” research in Scala 3 will provide an additional guardrail.

1 Like