Kotlin Confusions
Jumping from Java to Kotlin is easy, they said...
I was happily coding one day, when I came across this error in IntelliJ:
What gives? I can happily do the < 0
comparison, but not == 0
?
A colleague pointed out that <
will coerce 0
to a long, whereas ==
on a boxed primitive like Long
would be referential equality. Sure, I get that, but we’re not talking about a boxed primitive here. The duration.seconds
variable is a long
.
Although, the error message does say Long
… But then again, it’s also labelled 0
an Int
. Silly IntelliJ capitalisation. IntelliJ knows that it’s a long
:
(Yes, that’s a photo of a screen. Judge me. If you want to figure out how to take this screenshot on a mac, be my guest 😛).
And here are the docs for the method I’m using, com.google.protobuf.Duration#getSeconds()
:
So why does it seem like we’re dealing with a Long
?
(We’re using .seconds
instead of .getSeconds()
, because this is Kotlin, and Kotlin is a magical sugary Java.)
Maybe that’s the problem. Is Kotlin being magical and boxing this primitive?
To be sure it wasn’t an IntelliJ bug, I built manually:
And just to confirm the behaviour would ordinarily work in Java:
Yep, works in Java, borked in Kotlin.
So why does it work in Java? Because widening primitive conversions makes the 0
a long
.
…and Kotlin doesn’t do widening primitive conversions. As it turns out, Kotlin doesn’t have primitives at all. Cool! I probably should have known that 😅. Lesson: Kotlin is not Java, and I should’ve learned some more language basics earlier on.
So Kotlin is turning that original long
into a Long
. Okay, makes sense, and that explains why ==
didn’t work - referential equality requires the same types.
But wait. If type widening doesn’t happen, how on earth did < 0
work without issues?
<
is just syntactic sugar for compareTo()
, right… and the only method there is Long.compareTo(Long)
.
What gives?! Where is the magical coercion happening?
Let’s see if I can do compareTo
in IntelliJ:
Oh look, a bunch more functions, and one that takes an int. Weird.
Hang on, this makes sense. Kotlin isn’t Java. Kotlin brings in Extension Functions! They must have extended Long to have other compareTo
methods. Let’s click through and see:
Wait, no. That’s not an extension function on Long. It appears in the kotlin.Long
class… Oh. We’re not even using java.lang.Long
at all. Lesson: Kotlin is not Java.
We can also see that compareTo
is taking an Int
, not an int
- so that wasn’t an IntelliJ capitalisation error, that’s a legit type. Oh… yep, Kotlin is not Java.
We’ve made a similar mistake before in my team - assuming that Collection.*
in code referred to java.util.Collection
, or that it behaved in a similar way. This was unideal, because Kotlin’s Collections are immutable, so whenever we were adding an element to one, it was doing full copies of the collection.
If I had been a Better Developer with types, and added the L
to the initial comparison, I could have done something like duration.seconds == 200L
. I wouldn’t have seen the error, and I’d be setting myself up for bugs without realising it - because a referential equality check wouldn’t behave nicely there. It seems a bit dangerous that Kotlin would put people into that scenario.
… No, wait. Kotlin is not Java. ==
is not referential equality, it’s a call to .equals()
! Phew.
Wait, so why did == 0
fail, if .equals()
should be happy with any object? Let's try comparing it to a string:
IntelliJ recommends switching to ==
instead of equals()
. But it seems ==
does some extra (undocumented?) type checking at compile time… neat! No harm done here.
This means that 0L == 0
doesn’t compile, but 0L.equals(0)
does compile and will give false
.
In summary
Kotlin is not Java.