kotlinkotlin-java-interop

Broken null checking for Optionals?


I've encountered a situation where Kotlin's nullable type checking is doing the opposite of what I'd expect. Have I misunderstood something fundamental, or is the following not expected behavior?

The return of getOrNull for an Optional of a non-nullable type can be checked for null without the compiler complaining.

class Compiles_TypeNonNullable {
    fun foo(o : Optional<String>): String {
        val result = o.getOrNull() ?: "fallback"
        return result
    }
}

While trying to add a null check when the Optional is for a nullable type results in a compile error.

class DoesNotCompile_TypeNullable {
    fun foo(o : Optional<String?>): String {
        val result = o.getOrNull() ?: "fallback"
        return result
    }
}
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch: 
[ERROR] public fun <T : Any> Optional<TypeVariable(T)>.getOrNull(): TypeVariable(T)? defined in kotlin.jvm.optionals

This is the precise opposite of what I'd expect.

The above was observed with both Kotlin 1.9.10 and 1.9.23.


Solution

  • getOrNull only works on Optional<T>s where T is non-nullable. This is clear in it's declaration:

    public fun <T : Any> Optional<T>.getOrNull(): T? = orElse(null)
    

    The : Any constraint disallows any nullable types for T. : Any isn't technically needed here. The declaration still compiles without it, and it will return the wrapped non-null value or null if the optional is empty. But in Kotlin, Optional<T?> just doesn't make sense. The type constraint not only makes the intended usage clear, it also discourage people from using Optional<T?>.

    Here is why Optional<String?> doesn't make sense.

    An Optional<T> either:

    This makes sense for Optional<String> - it either contains a string or is empty. getOrNull returns the string if it is not empty, and returns null otherwise.

    According to the same logic, an Optional<String?> in Kotlin can be any of the following:

    The thing is, Optional cannot represent that second case. Internally, there is just a field of type T. If this field is null, the optional is empty, otherwise the optional contains an instance of T. Optional by design cannot distinguish between the second and third cases.

    To represent all 3 cases, you would need a Optional<Optional<String>> or similar.