Search code examples
scalarequireabstractobject-initialization

Assertions in abstract superclass scala creating NPE


The following code, when entered in REPL

abstract class A { val aSet: Set[Int]; require(aSet.contains(3)) }

class B extends A { val aSet = Set(4,5,6) }

new B()

gives a null point exception, rather than an invariant failure.

What would be the best idiom to solve this problem?


Similar questions:

Code Contracts: Invariants in abstract class

Private constructor in abstract class Scala?

and also online commentary: https://gist.github.com/jkpl/4932e8730c1810261381851b13dfd29d


Solution

  • When you declare a val, several things happen:

    1. The compiler makes sure that enough space is allocated for the variable when an instance of the class is initialized
    2. An accessor-method is created
    3. Initializers that setup the initial values of the variables are created.

    Your code

    abstract class A { val aSet: Set[Int]; require(aSet.contains(3)) }
    class B extends A { val aSet = Set(4,5,6) }
    new B()
    

    is roughly equivalent to

    abstract class A { 
      private var aSet_A: Set[Int] = null
      def aSet: Set[Int] = aSet_A
      require(aSet.contains(3)) 
    }
    
    class B extends A {
      private var aSet_B: Set[Int] = Set(4,5,6) 
      override def aSet: Set[Int] = aSet_B
    }
    
    new B()
    

    so, the following happens:

    1. Memory for aSet_A and aSet_B is allocated and set to null.
    2. Initializer of A is run.
    3. require on aSet.contains(3) is invoked
    4. Since aSet is overridden in B, it returns aSet_B.
    5. Since aSet_B is null, an NPE is thrown.

    To avoid this, you can implement aSet as a lazy variable:

    abstract class A { 
      def aSet: Set[Int]
      require(aSet.contains(3)) 
    }
    
    class B extends A {
      lazy val aSet = Set(4,5,6) 
    }
    
    new B()
    

    This throws the requirement failed exception:

    java.lang.IllegalArgumentException: requirement failed
    

    Obligatory link to Scala's FAQ:

    List of related questions:

    1. Why does implement abstract method using val and call from superclass in val expression return NullPointerException
    2. Overridden value parent code is run but value is not assigned at parent