No null safety when generics are in play?

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?

1 Like

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 Likes

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.
3 Likes

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.

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!

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
1 Like

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.

1 Like

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

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?.

More less