1. Introduction

Kotlin, being a versatile language, offers several utilities to make tasks more streamlined and intuitive. One such utility is the Comparator interface. In this tutorial, we’ll do a deep dive into the use of Comparator in Kotlin, and by the end of it, we’ll have a solid understanding of its capabilities.

2. What Is the Comparator Interface?

The Comparator interface is fundamental when controlling the ordering of objects. It’s essentially a tool we use to define custom ordering for objects, especially when the natural ordering isn’t sufficient.

Let’s look into the functions that are part of the Comparator interface and how we can use them to create custom ordering of objects.

3. The compare() Function

The primary function in the Comparator interface is the compare() function. Its job is to impose an order between two arguments of the same type:

abstract fun compare(a: T, b: T): Int

When we call the compare() function, it should return:

  • Zero if the two arguments are equal
  • A negative number if the first argument is less than the second
  • A positive number if the first argument is greater than the second

By implementing the compare() function in a way that the implementation abides by the above rules, we can create custom Comparator implementations. This enables us to perform custom sorting on Collections where the natural sorting is not adequate.

3.1. Sorting With the compare() Function

Let’s assume we have a List of words and we’d like to sort them by the length of each word:

val words = listOf("apple", "blueberry", "cherry", "date")
val lengthComparator = object : Comparator<String> {
    override fun compare(string1: String, string2: String): Int {
        return string1.length - string2.length
    }
}
assertIterableEquals(
    listOf("date", "apple", "cherry", "blueberry"),
    words.sortedWith(lengthComparator)
)

Here, we create a Comparator object and implement our custom logic to sort the words in its compare() function by comparing their lengths. We then pass this Comparator object to the sortedWith() method to sort our list with our desired sorting logic.

Note that this code is intentionally verbose for the sake of clarity. Let’s look at how we can simplify this code.

3.2. Simplifying the Code With Top-Level Functions

Kotlin offers us some nifty top-level functions to create Comparator objects that we can use to greatly simplify our code:

val words = listOf("apple", "blueberry", "cherry", "date")
assertIterableEquals(
    listOf("date", "apple", "cherry", "blueberry"),
    words.sortedWith( compareBy { it.length } )
) 

Here, we use the compareBy() top-level function to create the Comparator inline. We then use the Comparator to sort the words with our custom logic as before.

Similarly, we can use a Comparator to sort collections of user-defined objects:

val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Bob", 29))
assertIterableEquals(
    people.sortedWith(
        listOf(Person("Alice", 29), Person("Bob", 29), Person("Bob", 31)),
        compareBy({ it.age }, { it.name })
    )
)

In this example, we defined a class named Person with two properties: name and age. We then created a list of Person objects and used the compareBy() function to generate a Comparator that sorts them by age and then by name.

4. Comparator Extension Functions

Beyond the compare() function, the Comparator interface offers several extension functions. We can use these extension functions to perform operations like chaining Comparators or reversing the order of another Comparator. Let’s look at each of these functions one by one.

4.1. The reversed() Function

The reversed() extension function returns a Comparator that reverses the order of the original Comparator.

This function can be useful when we want to sort a list in the reverse order of a given Comparator. For example, we can sort a list of strings in the reverse order of their lengths:

val words = listOf("apple", "blueberry", "cherry", "date")
assertIterableEquals(
    listOf("blueberry", "cherry", "apple", "date"),
    words.sortedWith( compareBy<String> { it.length }.reversed() )
) 

In this example, we first create a Comparator that sorts the list of strings by their lengths in ascending order. We then reverse the order of the Comparator using the reversed() function. Finally, we sort the list of strings using the reversed Comparator.

4.2. The then() Function

The then() function returns a Comparator that compares two elements by the original Comparator and then by an additional Comparator if the original considers the two elements equal.

This is especially useful when we want to sort a collection by more than one criterion. Let’s look at an example:

data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Cleo", 29))
val sortedPeople = people.sortedWith(compareBy(Person::age).then(compareBy(Person::name)))
assertIterableEquals(
    listOf(Person("Alice", 29), Person("Cleo", 29), Person("Bob", 31)),
    sortedPeople
)

In the example above, we have a list of people, some of whom are the same age, but they all have different names. We want to sort the list by age and then by name.

To do this, we first call the compareBy() function to create a Comparator that compares people by age. Then, we call the then() function to chain another Comparator that compares people by name. Finally, we call the sortedWith() function to sort the list by the two criteria.

4.3. The thenBy() Function

The thenBy() function is similar to the then() function. The only difference between them is that thenBy() accepts a selector function that accepts a comparable value selector instead of a Comparator.

With this, we can make our code a bit more idiomatic. We can use the same property access syntax as the previous example without having to use the additional compareBy() function used to create the Comparator.

To demonstrate, let’s again sort a list of people by age and then by name:

data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Cleo", 29))

val sortedPeople = people.sortedWith(compareBy(Person::age).thenBy(Person::name))
assertIterableEquals(
    listOf(Person("Alice", 29), Person("Cleo", 29), Person("Bob", 31)),
    sortedPeople
)

In this example, much like the previous example with the then() function, we first call the compareBy() function to create a Comparator that compares people by age. We then call the thenBy() function to create a Comparator that compares people by name. We then call the sortedWith() function to sort the list of people with their names using the Comparator created by the thenBy() function.

4.4. The thenDescending() and thenByDescending() Functions

The thenDescending() function is similar to the then() function. The only difference is that the thenDescending() function reverses the order of the original Comparator.

Similarly, the thenByDescending() works like the thenBy() function except that the thenByDescending() function accepts a selector function instead of a Comparator and reverses the order of the original Comparator.

We can sort a list of people by age in ascending order and then by name in descending order using thenDescending():

data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Cleo", 29))

val sortedPeople = people.sortedWith(compareBy(Person::age).thenDescending(compareBy(Person::name)))
assertIterableEquals(
    listOf(Person("Cleo", 29), Person("Alice", 29), Person("Bob", 31)),
    sortedPeople
)

Similarly, we can use thenByDescending() and simplify the code even further:

val sortedPeople2 = people.sortedWith(compareByDescending(Person::age).thenByDescending(Person::name))
assertIterableEquals(
    listOf(Person("Bob", 31), Person("Cleo", 29), Person("Alice", 29)),
    sortedPeople2
)

4.5. The thenComparator() Function

The thenComparator() function creates a composite Comparator using the primary Comparator and another function that helps us perform an additional comparison inline.

For example, let’s use thenComparator() to sort a list of people by age and then by the length of their names:

data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Cleo", 29))

val combinedComparator = compareBy(Person::age)
    .thenComparator{ person1, person2 -> person1.name.length - person2.name.length }
val sortedPeople = people.sortedWith(combinedComparator)
assertIterableEquals(
    listOf(Person("Cleo", 29), Person("Alice", 29), Person("Bob", 31)),
    sortedPeople
)

Here, we use the thenComparator() function to create a single Comparator that first compares people by their age. We then use the thenComparator() function to perform another comparison by the length of their names. Finally, we call the sortedWith() function to sort the list of people using the combined Comparator we created with the help of the thenComparator() function.

5. Conclusion

With Kotlin’s Comparator interface, supplemented with its extension functions, we have the power to implement custom sorting easily. We can use these functions to enrich our code’s expression and write idiomatic Kotlin code. As always, the code samples can be found over on GitHub.

2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are closed on this article!