Kael Eppcohen
Kael Eppcohen

Reputation: 197

Kotlin Abstract Val Is Null When Accessed In Init Before Override

In Kotlin, accessing an abstract val in an init block causes a NullPointerException since the field is overridden by an extending class after the super class's init block executes.

The ideal solution would be a way to declare some code/function to execute after all stages of object instantiation are complete. I can only think of creating an initialize() function and manually calling it, which is bad because it's not automatic. Sticking it in init block doesn't work as shown in the below example.

As a comment pointed out below, instead of overriding fields, they can be passed in as parameters, but that doesn't work for my actual use-case. It adds a lot of clutter for object construction and is a nightmare when other classes try to extend it.

Below example shows a solution using coroutines. Waiting for a field to != null works in this case, but doesn't not when map is an open val with a default value that may or may not get overridden.

The problem is somewhat solved, but the solution is far from optimal. Any suggestions and alternative solutions would be greatly appreciated.

@Test @Suppress("ControlFlowWithEmptyBody", "SENSELESS_COMPARISON")
fun abstractValAccessInInitNPE() {

    val key = "Key"
    val value = "Value"
    abstract class Mapper {
        abstract val map: HashMap<String, String>
        fun initialize() { map[key] = value }
    }

    // Test coroutine solution on abstract mapper

    println("CoroutineMapper")

    abstract class CoroutineMapper: Mapper() {
        init {
            GlobalScope.launch {
                while (map == null) {}
                initialize()
            }
        }
    }
    val coroutineMapper = object : CoroutineMapper() {
        override val map = HashMap<String, String>()
    }

    val start = System.nanoTime()
    while (coroutineMapper.map.isEmpty()) {} // For some reason map == null doesn't work
    println("Overhead: ${(System.nanoTime() - start) / 1000000.0} MS")
    println("Mapped: ${coroutineMapper.map[key].equals(value)}")

    // Test coroutine solution on open mapper

    println("\nDefaultMapper")

    open class DefaultMapper: Mapper() {
        override val map = HashMap<String, String>()
    }

    val newMap = HashMap<String, String>()
    val proof = "Proof"
    newMap[proof] = proof

    val defaultMapper = object: DefaultMapper() {
        override val map = newMap
    }
    Thread.sleep(1000) // Definitely finished by the end of this
    println("Mapped: ${defaultMapper.map[proof].equals(proof) && defaultMapper.map[key].equals(value)}")

    // Basic solution (doesn't work)
    
    println("\nBrokenMapper")
    abstract class BrokenMapper: Mapper() {
        init { initialize() } // Throws NPE because map gets overridden after this
    }
    val brokenMapper = object: BrokenMapper() {
        override val map = HashMap<String, String>()
    }
    println("Mapped: ${brokenMapper.map[key].equals(value)}")
}

Upvotes: 2

Views: 1496

Answers (1)

Tenfour04
Tenfour04

Reputation: 93609

An open (as all abstract functions are) function should never be called from a constructor because then the class's initial state cannot be guaranteed in the superclass. It can lead to all kinds of very tricky bugs.

Usually there's a good way to design around this problem if you take a step back. For instance, instead of making the map an abstract property, make it a constructor parameter in the superclass. Then you know it's already initialized before subclass constructors can try to use it.

abstract class Mapper(key: String, value: String, val map: HashMap<String, String>)

abstract class DecentMapper(key: String, value: String, map: HashMap<String, String>) : Mapper(key, value, map) {
    init {
        map[key] = value
    }
}

val key = "Key"
val value = "Value"

val decentMapper = object : DecentMapper(key, value, HashMap()){
    //...
}

Upvotes: 5

Related Questions