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.
- The compiler might complain at
raw.toString()
that raw
may be null
.
- The compiler might complain that
Integer?
is not a permissible type parameter for Wrap
.
- 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:
raw.toString()
is allowed because toString()
is an extension for Any?
Integer?
is allowed substitution for T
because T
has nullable upper bound, namely Any?
- 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?
.