1. Overview
Kotlin builds on top of the Java Collection framework using extension functions. This dramatically improves usability and readability without the need for third-party dependencies such as Apache Commons or Guava.
In this tutorial, we’ll focus on sorting in Kotlin. Also, we’ll use the kotlin.comparisons package to implement complex ordering rules.
2. Sorting a Collection
Kotlin provides multiple utilities to make the process of sorting collections easier. Let’s explore several of these functions.
2.1. sort()
The simplest way to sort a collection is to call the sort() function. This function will use the natural order of the elements. Also, it’ll order in ascending direction by default, so ‘a’ is before ‘b’ and ‘1’ is before ‘2’:
val sortedValues = mutableListOf(1, 2, 7, 6, 5, 6)
sortedValues.sort()
log.info("$sortedValues")
And the result of the above code is:
[1, 2, 5, 6, 6, 7]
It’s important to note that we’ve used a mutable collection. The reason is that the sort function will sort in-place.
Furthermore, we can use the sortDescending() or reverse() functions for sorting in descending order*.*
2.2. sortBy()
If we need to sort by specific properties of a given object, we can use sortBy. The sortBy function allows us to pass a selector function as an argument. The selector function will receive the object and should return the value on which we’d like to sort:
val sortedValues = mutableListOf(1 to "a", 2 to "b", 7 to "c", 6 to "d", 5 to "c", 6 to "e")
sortedValues.sortBy { it.second }
log.info("$sortedValues")
And the result of the above code is:
[(1, a), (2, b), (7, c), (5, c), (6, d), (6, e)]
Again, the collection needs to be mutable because the sortBy() function will sort in-place.
Like before, for descending order, we can use the sortByDescending() or reverse functions.
2.3. sortWith()
For a more advanced usage (to combine multiple rules, for example), we can use the sortWith() function.
We can pass a Comparator object as an argument. In Kotlin, we have multiple ways to create Comparator objects, and we will cover that in the next section:
val sortedValues = mutableListOf(1 to "a", 2 to "b", 7 to "c", 6 to "d", 5 to "c", 6 to "e")
sortedValues.sortWith(compareBy({it.second}, {it.first}))
log.info("$sortedValues")
And the result of the above code is that they are sorted by letter and then by number:
[(1, a), (2, b), (5, c), (7, c), (6, d), (6, e)]
Because the sortWith() will do the sorting in-place, we need to use a mutable collection. For descending order, we can use the reverse function or alternatively define the right Comparator.
3. Comparison
Kotlin contains a very useful package to build a Comparator – kotlin.comparisons. In the following sections, we’ll discuss:
- Comparator creation
- Handling of null values
- Reversing the order
- Comparator rules extension
3.1. Comparator Creation
In order to simplify the creation of our Comparator, Kotlin brings many factory functions to make our code more expressive.
The simplest Comparator factory available is naturalOrder()**. No arguments are needed, and the order is ascending by default:
val ascComparator = naturalOrder<Long>()
For objects with multiple properties, we can use the compareBy() function. As arguments, we give a variable number of functions (sorting rules) that will each return a Comparable object. Then, those functions will be called sequentially until the resulting Comparable object evaluates as not equal or until all functions are called.
In the next example, it.first value is used for comparisons and, only when values are equal, it.second will be called to break the tie:
val complexComparator = compareBy<Pair<Int, String?>>({it.first}, {it.second})
Feel free to explore kotlin.comparisons to discover all the available factories.
3.2. Handling of null Values
A simple way to improve our Comparator with null value handling is to use the nullsFirst() or nullsLast() functions. These functions will sort null values in first or last place, respectively:
val sortedValues = mutableListOf(1 to "a", 2 to null, 7 to "c", 6 to "d", 5 to "c", 6 to "e")
sortedValues.sortWith(nullsLast(compareBy { it.second }))
log.info("$sortedValues")
The result of the above code will be:
[(1, a), (7, c), (5, c), (6, d), (6, e), (2, null)]
We can see that the last value in the resulting collection is the one with null value.
3.3. Reversing the Order
To reverse the order, we can use the reverseOrder() function or the reversed() function*.* The former function has no arguments and returns a descending order. The latter function can be applied to a Comparator object, and it will return its reversed Comparator object.
To build a Comparator using descending natural order we can do:
reverseOrder()
3.4. Comparator Rules Extension
Comparator objects can be combined or extended with additional sorting rules via the then* functions available in kotlin.comparable package.
Only when the first comparator evaluates to equal, the second comparator will then be used.
Our list of students contains an age and a name for each individual. We want them sorted from youngest to oldest and, when they are of the same age, descending based on the name:
val students = mutableListOf(21 to "Helen", 21 to "Tom", 20 to "Jim")
val ageComparator = compareBy<Pair<Int, String?>> {it.first}
val ageAndNameComparator = ageComparator.thenByDescending {it.second}
students.sortWith(ageAndNameComparator)
log.info("$students")
The result of the above code will be:
[(20, Jim), (21, Tom), (21, Helen)]
4. sorted(), sortedBy(), and sortedWith()
So far, we’ve mainly discussed three sorting functions: sort(), sortBy(), and sortWith(). In addition to these functions, Kotlin offers sorted(), sortedBy(), and sortedWith() functions for sorting purposes. They have similar names and the same parameter definitions.
So, let’s figure out the differences between sort*() and sorted*() functions.
Let’s first look at the definitions of sort*() functions:
public actual fun <T : Comparable<T>> MutableList<T>.sort(): Unit {...}
public inline fun <T, R : Comparable<R>> MutableList<T>.sortBy(crossinline selector: (T) -> R?): Unit {...}
public actual fun <T> MutableList<T>.sortWith(comparator: Comparator<in T>): Unit {...}
As we can see, sort*() functions have some common characteristics:
- They’re extension functions of MutableList. *They’re only available for MutableLists*.
- They return Unit. As we noted earlier, they perform in-place sorting.
Next, let’s move to the definitions of sorted*() functions:
public fun <T : Comparable<T>> Iterable<T>.sorted(): List<T> {...}
public inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy(crossinline selector: (T) -> R?): List<T> {...}
public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> {...}
These are the characteristics of sorted*() functions:
- They’re extension functions of Iterable. They’re available for both mutable and read-only collections.
- They return a List. They don’t modify the receiver Iterable object but return the sorted result as a new List.
In summary, if our input collection is read-only, we can only obtain the sorted result using sorted*() functions. When sorting a mutable collection, we have two options: using sorted*() to obtain a new sorted List and keeping the original collection unchanged or calling sort*() for in-place sorting.
5. Conclusion
In this article, we learned how to use the sort(), sortBy(), and sortWith() functions to sort collections in Kotlin. We also used kotlin.comparisons package to create Comparator objects and enhance them with additional sorting rules.
Additionally, we discussed the differences between sort*() functions and sorted*() functions.
The implementation of all of these examples and snippets can be found over on GitHub.