Intro
Previously we discussed the idea of checking equality of 2 objects which answer the question "are these 2 objects equal of not" with proper answers true
("Yes, they are equal to each other") and false
("No, they are different from each other").
It’s useful for operations like contains
, distinct
and many others.
More on that here.
However, some operations require a bit more flexibility. It would be hard to infer order of some elements by just knowing are these elements equal or not. So we need different API for that. Let’s find out how it was done in Java and Kotlin (and briefly look into another options)
Basic types
Java
Most[1] basic primitive types operate with following comparison operators: >
(more than), >=
(more or equal), <
(less than) and <=
(less or equal)
public class Main {
public static void main(String[] args) {
System.out.println(5 > 4); // int comparison
System.out.println(5L >= 4L); // long comparison
System.out.println(4.0 <= 5.0); // double comparison
System.out.println(4f < 5f); // float comparison
System.out.println('a' < 'c'); // char comparison
// System.out.println(true > false); // primitive boolean comparison doesn't exist in Java
byte b1 = 5;
byte b2 = 4;
System.out.println(b1 > b2);
short s1 = 5;
short s2 = 4;
System.out.println(s1 >= s2);
}
}
Based on them some useful functions can be constructed.
As example, Math.max and Math.min functions are defined for double
, float
, int
and long
types[2][3]
This approach is a bit limited in functionality however wrapper types for all basic types got more generic treatment that can be used as far as you are fine with potential boxing/unboxing of primitives.
Kotlin
Kotlin doesn’t have explicit difference basic and non-basic types so proper explanation will be done later. However, it’s worth noting a couple of important points:
-
Comparison operators are working for all basic types including Boolean and 4 Kotlin-specific unsigned number types:
fun main() {
println(5 > 4) // Int comparison
println(5L >= 4L) // Long comparison
println(4.0 <= 5.0) // Double comparison
println(4f < 5f) // Float comparison
println('a' < 'c') // Char comparison
println(false <= true); // Boolean comparison
val b1: Byte = 5
val b2: Byte = 4
println (b1 > b2)
val s1: Short = 5
val s2: Short = 4
println (s1 >= s2)
println(4u < 5u) // UInt comparison
println(4uL <= 5uL) //ULong comparison
val ub1: UByte = 5u
val ub2: UByte = 4u
println(ub1 > ub2)
val us1: UShort = 5u
val us2: UShort = 4u
println(us1 >= us2)
}
Comparable
Java
While basic type comparison provides us some foundation, it’s not exactly sufficient: you need to write same function for multiple types and also non-primitive types are not covered by this approach. However, there is another solution: Comparable
Comparable
is the interface with single method to implement - compareTo, description of which says the following:
Compares this object with the specified object for order. Returns a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.
To simplify things let’s have Integer type as example.
If we have var i1 = Integer.valueOf(4)
, var i2 = Integer.valueOf(6)
and var i3 = Integer.valueOf(4)
we can expect the following
-
i1.compareTo(i2)
returns some negative number. It can be just-1
or-2
or any other negative number, evenInteger.MIN_VALUE
is fine according to method contract. -
i2.compareTo(i1)
returns some positive number. Again, it can be1
,2
or evenInteger.MAX_VALUE
, it doesn’t matter. -
i1.compareTo(i3)
should return zero as it’s the same number in terms of comparison[4]
This API is slightly unobvious, but it still can provide us a great abstraction that we can use in a lot of places. Examples for that would be:
-
Sorted collections like TreeSet, TreeMap, ConcurrentSkipListSet or ConcurrentSkipListMap
List of examples is definitely not excessive.
In general, you can imagine that everything that needs comparison of elements and so-called natural order is sufficient can rely on Comparable
interface.
So if you have binary search, median, 90-percentile or any other thing you can use this interface in Java.
At this point there might be a reasonable question "How to implement this interface properly," but we will talk about that a bit later.
Kotlin
Same as in Java, Kotlin also has own Comparable interface to implement with the same contract around compareTo implementation, however, there are more ways to use this interface out of the box. We can always use everything from Java[5] but there are more convenient alternatives in Kotlin and many new things. Let’s start with mostly familiar stuff:
-
minOf and maxOf for 2, 3 or any amount of comparable elements
-
maxOrNull
andminOrNull
extension functions for iterables and sequences and any array type[6] -
sorted extension functions for iterables, sequences and any array type (return new container with sorted elements)
-
sort extension function for MutableList and any array type (sort elements in place)
As mentioned, there are some things that are not present in Java (at least in the same way):
-
Any
Comparable
type have the ability to use comparison operators (so you can write expression like"abc" < "cde"
[7]) -
Coercion extension functions like coerceIn, coerceAtLeast and coerceAtMost (with coercion functions you can write something like
inputValue.coerceIn(minReasonableValue, maxReasonableValue)
) -
ClosedRange type
-
-By counterparts for sorting functions like sortBy or sortedBy. These functions allow us to easily sort list, sequence or other type by specific property which implements
Comparable
. It looks likeworkers.sortedBy { it.salary }
-
-By counterparts for min / max functions. Example code would be something like
val mostValuableWorker = workers.maxByOrNull { it.salary }
-
Descending counterparts for sorting functions like sortDescending, sortedDescending or sortedByDescending. We can extend previous example to find 10 most valuable workers:
val mostValuableWorkers = workers.sortedByDescending { it.salary }.take(10)
And probably many more! There is nothing that you cannot do in Java in some way, but it’s quite a lot of convenient things out of the box!
Comparator
Java
Comparable
type is convenient when your class have some universal meaning of order.
Of course, that’s not really the case for all types. What’s the proper way to order users? By name? Id? Maybe age? Or even name at first and age after that? Nobody knows for sure in advance.
And even for types with some kind of agreed order, this order cannot always fit our task. As example, while String have universally agreed to be ordered by lexicographic order, in some situations we want strings to be ordered by string length first or by Scrabble score of this string
To solve this issue Comparator type can be introduced.
In idea, it’s pretty similar to Comparable
: interface with one method to implement.
Now it’s compare which has quite similar contract as compareTo
from Comparable
but accepts 2 arguments instead of one.
Upside of using Comparator
is the idea that we can decide our ordering logic at the moment of sorting or finding the maximum.
Downside is that we need to explicitly provide Comparator
in specific call.
Let’s check some examples of Java API that uses Comparator
:
-
Comparator
can be provided to all sorted collections mentioned before to avoid natural order requirement. Example can be found here[8] -
All mentioned sorting functions and methods have the option to provide
Comparator
: lists, arrays and streams -
Mentioned min and max for collections also have their analogue with explicit
Comparator
provided
And as always, there is no limit for possibilities to use this type.
It was previously mentioned that we can do the in some way the same thing that was possible with Kotlin. Let’s try to write same example code in Java:
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args){
List<Worker> workers = fetchWorkers();
Comparator<Worker> workerComparator = Comparator.comparing(Worker::getSalary);
var sortedBySalary = workers.stream()
.sorted(workerComparator)
.toList();
var mostValuableWorker = workers.stream()
.max(workerComparator);
var mostValuableWorkers = workers.stream()
.sorted(workerComparator.reversed())
.limit(10L)
.toList();
}
}
Kotlin
Same as before, Kotlin have own Comparator with the same contract to implement.
Because of existence of functions with natural order selector (-By functions like sortedBy
or maxByOrNull
) comparators used a bit less often, but they are still useful if you need more complex ordering (as example - sort something by name and age)
Also, same as before, you can use everything from Java but Kotlin-friendly alternatives are provided:
-
-With counterpart for
sort
function - sortWith (sort in-place with provider comparator) -
-With counterpart for
sorted
function - sortedWith (return new container with elements sorted by provided comparator) -
-With counterpart for
max
andmin
functions like maxWithOrNull and minWithOrNull
Our code example from Java can be written like this:
import kotlin.comparisons.compareBy
fun main() {
val workers: List<Worker> = fetchWorkers()
val workerComparator: Comparator<Worker> = compareBy { it.salary }
val sortedBySalary = workers.sortedWith(workerComparator)
val mostValuableWorker = workers.maxWithOrNull(workerComparator)
val mostValuableWorkers = workers.sortedWith(workerComparator.reversed()).take(10)
}
While in this scenario it’s easier to stick with -By functions:
fun main() {
val workers: List<Worker> = fetchWorkers()
val sortedBySalary = workers.sortedBy { it.salary }
val mostValuableWorker = workers.maxByOrNull { it.salary }
val mostValuableWorkers = workers.sortedByDescending { it.salary }.take(10)
}
With more complex scenario it would be complicated to use -By functions. So, back to -With functions, note the change of comparator here:
import kotlin.comparisons.compareBy
fun main() {
val workers: List<Worker> = fetchWorkers()
val workerComparator: Comparator<Worker> = compareBy({ it.salary }, { it.name })
val sorted = workers.sortedWith(workerComparator)
val mostValuableWorker = workers.maxWithOrNull(workerComparator)
val mostValuableWorkers = workers.sortedWith(workerComparator.reversed()).take(10)
}
How to create Comparator?
Java
Prior to Java 8, you can only create one manually (creating anonymous class with compare being implemented) with built-in tools (of course there is a Guava option outside Java’s standard library)
After Java 8 release, you still have an option to implement it manually (however, now you can use shiny lambdas because Comparator is functional interface):
public class Main {
public static void main(String[] args) {
Comparator<Worker> comp = (a, b) -> Integer.compare(a.salary, b.salary);
}
}
However, as demonstrated before, in most cases there is a better option - using Java 8 Comparator static and default methods like comparing and similarly named function for extracting primitives:
public class Main {
public static void main(String[] args) {
Comparator<Worker> comp = Comparator.comparingInt(Worker::getSalary);
}
}
It is not always the shortest function to write, but it is mostly likely to be the most readable option.
To combine 2 comparators thenComparing method can be used:
public class Main {
public static void main(String[] args) {
Comparator<Worker> workerSalaryComparator = Comparator.comparingInt(Worker::getSalary);
Comparator<Worker> workerAgeComparator = Comparator.comparingInt(Worker::getAge);
Comparator<Worker> workerComparator = workerSalaryComparator.thenComparing(workerAgeComparator);
}
}
If all key extractors (selectors in Kotlin) known in advance they can be chained with thenComparing method and similarly named primitive type methods:
public class Main {
public static void main(String[] args) {
Comparator<Worker> workerComparator = workerSalaryComparator
.comparingInt(Worker::getSalary)
.thenComparingInt(Worker::getAge);
}
}
To reverse order of comparator reversed method can be used. If reversed natural order is desired there is reversedOrder function to use.
Lastly, to prioritize or deprioritize nulls while working with objects of some type nullsFirst or nullsLast functions can be used.
Kotlin
Same as in Java, Comparator
can be implemented manually, however it’s better to use compareBy from kotlin.comparisons package
import kotlin.comparisons.compareBy
fun main() {
val comparator: Comparator<Worker> = compareBy { it.salary }
}
To chain multiple selectors for predicate, either compareBy with multiple arguments or thenBy extension can be used
import kotlin.comparisons.compareBy
import kotlin.comparisons.thenBy
fun main() {
val comparator: Comparator<Worker> = compareBy({ it.salary }, { it.age })
val comparatorAlt: Comparator<Worker> = compareBy { it.salary }.thenBy { it.age }
}
First approach is a bit easier, but second is more flexible. As example, if we need to reverse comparison by age only, we can use thenByDescending:
import kotlin.comparisons.compareBy
import kotlin.comparisons.thenByDescending
fun main() {
val comparator: Comparator<Worker> = compareBy { it.salary }.thenByDescending { it.age }
}
To chain multiple existing comparators, then or thenDescending can be used:
import kotlin.comparisons.compareBy
import kotlin.comparisons.then
import kotlin.comparisons.thenDescending
fun main() {
val workerSalaryComparator: Comparator<Worker> = compareBy { it.salary }
val workerAgeComparator: Comparator<Worker> = compareBy { it.age }
val workerNameComparator: Comparator<Worker> = compareBy { it.name }
val workerComparator = workerSalaryComparator then workerAgeComparator thenDescending workerNameComparator
}
Same as in Java, we have reversed extension function and reverseOrder function.
How to implement Comparable?
Java
While Java provides very convenient API to create comparators, it doesn’t really helps with implementing Comparable interface in your classes.
You can implement it manually:
record Worker(int id, double salary, int age) implements Comparable<Worker> {
@Override
public int compareTo(Worker other) {
return Integer.compare(this.id, other.id);
}
}
Or to avoid possible errors you can have underlying comparator to help with your Comparable implementation:
record Worker(int id, int salary, int age, String name) implements Comparable<Worker> {
private static Comparator<Worker> comp = Comparator.comparingInt(Worker::id);
@Override
public int compareTo(Worker other) {
return comp.compare(this, other);
}
}
Kotlin
Thing which is different from Java - you also have compareValuesBy function which can help to implement Comparable interface:
import kotlin.comparisons.compareValuesBy
data class Worker(val id: Int, val salary: Int, val age: Int, val name: String): Comparable<Worker> {
override operator fun compareTo(other: Worker): Int = compareValuesBy(this, other) { it.id }
}
Note that usage of compareValuesBy
is rather limited so potential ideas of using Comparator
or implementing Comparable
manually still can be valid.
Conclusion
In general there is nothing unique in Kotlin: we have the same concept of Comparable
and Comparator
with the same contract and the same idea how to use it. Most differences are due to two facts. First is, primitive functions are not always separated. Because of that you don’t need to worry whether to using something like comparing
or comparingInt
. It’s hard to easily solve this issue right now, but maybe we will see something cool with Project Valhalla. Second is, standard library is just richer. Of course, that can alleviated with usage of external libraries (as example, you can have much richer API by using StreamEx which include methods like sortedBy
or maxBy
)
However, I feel like these small differences made working with Kotlin quite a lot easier. Even with external libraries it’s hard to work with existing types like Stream
, Collection
and other. You can import some helper library (technically Kotlin standard library can be used as helper library for Java) but it less convenient as you need to write something like take(sortedWith(workers, (worker) → worker.getSalary()), 10)
. Right now you can work around this problem by adding Manifold to your project but that can introduce another set of problems and in general it’s not a popular solution. Hopefully we will see something like extension functions or pipe operator in Java in the future.
I hope you found something useful over this article.
Sidenote: Design discussion about int comparison result
kinda subjective |
Kotlin chosen Int
type for compare
and compareTo
result for easier interop with Java.
Java chosen int
type for compare
and compareTo
result due to lack of other viable options (enums were introduced only in Java 1.5) and to follow steps of C/C++
In this context both decisions look reasonable but without this context it’s quite weird to operate with whole integer type to represent 3 possible states. It’s not that hard to memoize contract of comparisons in Java/Kotlin, but it’s just inconvenient to works with.
Ideally it would be nice to represent comparison result as enum with 3 possible values: enum Ordering { Less, Equal, Greater }
It is more readable, there is less confusion how to implement it manually (NB: still would not recommend, please use helper functions if possible) and also it is easier to match result:
public class Main {
public static void main(String[] args) {
var result = Integer.compare(1, 2);
var text = switch(result) {
case Less -> "Result is less than expected";
case Equal -> "This is what we needed";
case Greater -> "Result is more than expected";
};
System.out.println(text);
}
}