Comparator missing declaration-site variance?

Why is Comparator<T> not Comparator<in T>?

2 Likes

Good question!

I don’t have an answer, but my guess would be some issue with Java compatibility.

As you may know, in Java, you can only specify variance where a type is used, not where it’s declared. If you look at java.lang.Comparator, you’ll see that it’s defined as interface Comparator<T> — as there’s no other way to do it. Instead, a few of its methods have wildcards, (e.g. thenComparing(Comparator<? super T> other)).

Kotlin, on the other hand, allows declaration-site variance too, in the way that you suggest. That builds knowledge about the variance into the class declaration, and avoids a lot of extra typing (and thinking) wherever it’s used.

Kotlin/JVM makes very heavy use of the existing Java standard library. In a few cases, it uses its own wrappers with tweaks to make things easier to use in Kotlin — for example, adding String.indices() so you can loop over the characters in a String. That even extends to adding variance where appropriate; for example kotlin.collections.List is defined as interface List<out E> : Collection<E> (unlike the sub-interface MutableList, which is invariant.)

But, as you point out, they didn’t do so for kotlin.Comparator.

The direct reason is that that’s not a wrapper class, but a simple typealias for java.util.Comparator. So it doesn’t have the option of changing variance, or anything else.

However, I don’t know why they didn’t make it a wrapper class. Using variance in the way you suggest is an obvious improvement — in fact, section 9.3.4 of the book Kotlin In Action (which was written by two members of the Kotlin development team, and is excellent!) gives interface Comparator<in T> as an example when explaining contravariance!

Maybe they felt that ultimately a wrapper wasn’t justified; or maybe it revealed some variance problem in the Java class that couldn’t be fixed that way; or maybe it would cause some other problem interoperating with Java code that defines or uses comparators.

Note that you can still specify the variance when using a Comparator, e.g.:

 val c: Comparator<in String> = //…
3 Likes

Thank you for your long reply.
The java.util.Comparator interface has wildcards, but all wildcards are ? super T which perfectly fits contravariance. Comparator is one of the most prominent interfaces, so if it can’t be solved with a clean typealias I would have expected some intrinsic solution of which you have named a few.

1 Like

Maybe it’s worth more investigation and a youtrack issue?

Testing around a little, the problem is actually the thenComparing methods. The fact that T is used as a parameter is breaking the contravariance. Kotlin has extension methods for this (called then), so they are not actually needed and are just breaking things.
I guess it is too complicated to hide them in Kotlin and cover all the corner cases, like a comparator implementation that choses to override the default thenComparing method (unlikely but possible).
Java 8 introduced these methods, so I wouldn’t be suprised if Kotlin originally provided contravariance on Comparator and had to give it up for Java 8 interoperability.

3 Likes