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)
}
  • While technically there is no need to do that, for performance’s sake you can find some specific functions for basic types that corresponds to Java’s primitive types like max, min or more a bit more generic minOf and maxOf from kotlin.comparisons package

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, even Integer.MIN_VALUE is fine according to method contract.

  • i2.compareTo(i1) returns some positive number. Again, it can be 1, 2 or even Integer.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:

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 and minOrNull 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 like workers.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

  • max and min functions can also be found for Stream type

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 and min 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.

Also, same as in Java, Kotlin has nullFirst and nullLast functions

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);
    }
}

Note that in some languages where that design is already in place. Examples are: Haskell, Rust.


1. Actually all of them except void type (which is not always considered as primitive type) and boolean. That types are: char, byte, short, int, long, float and double
2. it’s relatively common to see byte, short and char types omitted while implementing common functions
3. It’s also worth mentioning that floating point types sometimes have slightly different idea of comparison in some corner cases. As example comparison operators treat negative zero and positive zero as equal value but min and max functions treat positive zero as greater value comparing to negative zero
4. In this example it’s also the same number in terms of equality. Usually it’s expected that a.compareTo(b) == 0 implies a.equals(b) and vise versa. However, in rare cases this assumption is not correct. One example from standard library would be BigDecimal. equals of BigDecimal implies same value and scale but compareTo equal to zero implies only numerical equality (so <2.00>.equal(<2.0>) returns false but <2.00>.compareTo(<2.0>) returns 0
5. Assuming Kotlin JVM target. Otherwise, you still will have Kotlin alternatives mostly for everything except maybe sorted collections
6. There are also max and min functions but there are in relatively long process of deprecation-removal-return-with-different-signature ¯_(ツ)_/¯
7. Technically you can extend any type by providing operator function compareTo without implementing Comparable type explicitly. However, it only allows for operator usage, so it’s better to just implement Comparable if that’s possible and suitable for specific type. More on that here
8. While it’s enforced neither by SortedSet nor SortedMap interface (because they cannot enforce constructors) it’s recommended by these interfaces to have this constructor and standard library classes follow this recommendation