NullPointerException by design or bug?

Hi everyone.

I just stumbled over a code construct which let’s me wonder why it causes a NullPointerException.

class Container {
    public var lookup:HashMap<Int, String?>? = null
    
    public fun initMap() {
        val lookup = HashMap<Int, String?>()
        lookup[1] = "Test"
        lookup[2] = null
        this.lookup = lookup
    }
}


fun getContainer(): Container? {
    val rnd = kotlin.random.Random.nextDouble()
    return when {
        rnd < 0.3 -> { // container with map
            val container = Container()
            container.initMap()
            container
        }        
        else -> null
    }
}

fun getText(i:Int): String? {
    return getContainer()?.lookup!![i]
}

fun main() {
    println(getText(1) ?: "null")
    println(getText(2) ?: "null")
}

My overall code is a bit more complex and has more variations but it this stripped version shows my concern. Running this code, get’s me a NullPointerException in the getText method. Both the Container and the lookup are potentially null. For the getContainer I have null-safety because I expect at this place it could be null. But for the lookup I know that it is not null in this case, so I use the !! to handle the access.

I am aware that the overall code could maybe be rewritten to maybe satisfy Kotlin, but that’s not my intention. I would like to understand why Kotlin seems to be accessing lookup on a null member with such a constellation. I am transpiling some other codebase to Kotlin and would need to handle such special accesses, for this I need to understand the logic when Kotlin accesses members despite the ?. operator.

In most other languages the ?. access to the container would already ensure that the overall expression is null knowing no access to lookup can be performed. But it seems that Kotlin tries to access lookup anyhow and this causes a runtime error which seems not to be valid.

Other languages (e.g. C# and TypeScript):

  1. getContainer() is called and returns null
  2. Due to the fact of getContainer returning null the member access expression is already skipped and the overall expression is null.

In Kotlin:

  1. getContainer() is called and returns null.
  2. The member access to lookup is done, but due to null-safe this the expression getContainer()?.lookup evaluates to null.
  3. The not-null expression !! is evaluated on null and this causes the NPE

If my assumption is correct I would like to understand what’s the rationale behind evaluating the overall expression further despite knowing already that the result is null.

Based on that I will need to update my transpiler, even though I don’t know yet how an alternative expression could look like to handle the two nullability cases within one access chain. Likely I need to promote all !! to ? accesses if there is a previous ? access in the chain.

Your interpretation is correct.

I’m not sure about the rationale, but it may be related to the fact that Kotlin allows extension functions on nullable receiver. Such functions should be still executed in such chain of calls.
https://kotlinlang.org/docs/extensions.html#nullable-receiver

Some options to use in your specific example:

fun getText(i:Int): String? {
    return getContainer()?.let { it.lookup!![i] } 
}
fun getText(i:Int): String? {
    val container = getContainer() ?: return null
    return container.lookup!![i]
}
fun getText(i:Int): String? {
    return getContainer()?.lookup?.get(i)
}

If you rewrote your code so that lookup is not nullable, you would notice that you still cannot use the indexed access operator directly, because getContainer()?.lookup is nullable, however you could remove !! operator in the examples above.

Thanks for confirming my assumption. At least for some places I was able to get now variant 3 of your solution proposal. The other two are hard to achieve in my use case of transpiling from a different codebase. I have expressions like getContainer()?.lookup[index] in my TypeScript codebase and rewriting it to let or additional locals is not easy so I went for the approach of having a ?.get() call now.

The nullable receiver feature sounds reasonable why further evaluation might be done. I guess Kotlin could detect my scenario (as there is no nullable receiver) and rather emit code which results in a null result of the expression, but it might also be rather a corner case which does not justify the effort of maintaining the detection of it.

Unfortunately the ?.get() solution might hide some scenarios where container is not null but the lookup is, but for the actual application logic this should not matter. It rather hides that some expectations are maybe not fulfilled which could indicate a bug.

Let’s see if some of the involved experts in this feature/behavior can shed some additional light to it :smile:

Just to add a runnable example for people to play with:

fun nullableFun1(): String? = "not null".also { println("1") }
val String.nullableProp2: String? get() = null.also { println("2") }
fun String.nullableFun3(): String? = "not null".also { println("3") } // Try swapping out "not null" and 'null' to pick which step will eval to null.
val String.nullableProp4: String? get() = "not null".also { println("4") }

fun main() {
    nullableFun1()
        ?.nullableProp2
        ?.nullableFun3()!!
        .nullableProp4 // In your example, this is an `operator fun get(index: Int)`

    println("Done")
}

As you noticed, Kotlin is not halting evaluation for the entire line but instead returns null for each step and continues.

I’m not well experienced with null-safe calls in other languages but to me, it feels pretty natural to return null instead of assuming evaluation should stop. Just because I got a null along the way does not mean the rest of my expression is useless, I need that null! :stuck_out_tongue_winking_eye:

^ That feeling of mine is probably due to how useful null is in Kotlin (which isn’t pure null for declarations since you’re working with null UNION SomeOtherType). Maybe in those languages, once you get null you’re likely done with the expression? And just to round out the idea: How does one perform nullsafe calls and continue in TypeScript or C#? EDIT: I imagine lots of parentheses would be required.

1 Like