No null safety when generics are in play?


#1

Consider this simple code:

data class Wrap<T>(var raw: T) {
    val size
        get() = raw.toString().length
}

fun main(args: Array<String>) {
    val test: Wrap<Integer?> = Wrap(null)
    println(test.size)
}

I expect some kind of failure here.

  1. The compiler might complain at raw.toString() that raw may be null.
  2. The compiler might complain that Integer? is not a permissible type parameter for Wrap.
  3. We get an NPE when executing test.size.

We get … nothing. The code runs, printing an empty line.
(When calling from Java, due to silently added platform types (I assume?) things even get a more interesting.)

What’s going on here?


#2

I’ve been caught out by this myself, and the reason is hidden in the documentation at https://kotlinlang.org/docs/reference/generics.html

The default upper bound (if none specified) is Any?.

i.e. T in your example is always treated as an optional.

Simply by changing T to be bound to Any it will add in the null checks you expect to the Wrap class.


#3

Are you sure you get an empty line? Because I get “4” printed. Which is the length of the string "null" that is the result of null.toString()

None of the failures or compiler errors should happen in your code:

  1. raw.toString() is allowed because toString() is an extension for Any?
  2. Integer? is allowed substitution for T because T has nullable upper bound, namely Any?
  3. NPE doesn’t happen, again because toString() is an extension that can handle nullable recevier.

#4

Thanks for the reference! I suspected something like that but couldn’t get it to match that toString() would be accepted on an optional type.


#5

Ah, me too. Sorry, my bad. Must have gotten lost in a series of prints.

I think this is the core of my confusion. It’s surprising to me that null should accept any method call; it certainly doesn’t in Java! But Wrap is even fine when we call it from Java, passing it Java null.

I remain puzzled why this would be Kotlin’s semantics, but thanks for the explanation!


#6

null does not allow to call any method on it. Only the extensions that allow nullable receivers can be invoked on null receiver, e.g:

fun Any?.toString(): String
fun String?.equals(other: String?, ignoreCase: Boolean = false): Boolean
fun T.let(f: (T) -> R): R // because T is generic with Any? upper bound

#7

Remember to always write:

data class Wrap<T : Any>(var raw: T)

by default generic types allow implicitly on null values like this:

data class Wrap<T {{: Any?}}>(var raw: T)

then compiler will ensure that everywhere you use T you will only have non-nullable types.


#8

I meant, “any at all”. Anyway, "null as a value" will take some getting used to, especially in situations of interop.


#9

Thanks. I think I got foreign-languaged; in Swift, T is equivalent to T: Any and you write T? to get the equivalent of T: Any?.


#10

More less