Collection processing in Kotlin: Sorting, shuffling and reversing
This is a chapter from the book Functional Kotlin. You can find it on LeanPub or Amazon. It is also available as a course.
To have your collection elements organized in a concrete order, we can use sorting functions: sorted
, sortedBy
and sortedWith
.
sorted
can only be used on a list of elements with natural order for elements that implement the Comparable
interface. The most important types with natural order are:
Int
,Long
,Double
and other basic classes representing numbers that are sorted from the lowest number to the highest.Char
is treated as a number in UTF-16 code under the hood, so comparing two characters is like comparing their codes. Letters are organized in alphabetical order, but all capital letters come before all lowercase letters. A space comes before all letters.String
, whose natural order is lexicographical (this is a generalization of the alphabetical order that is used in dictionaries), where we start from comparing the first character (according toChar
order); whenever two characters are equal, we are shifting the burden of the decision to the next character.Boolean
placesfalse
beforetrue
. This is becausefalse
andtrue
are typically represented by0
and1
, respectively, and the natural order for numbers places0
before1
.
To reverse the order of the elements in the list, use the reversed
method.
To reverse the sorting order, we can use the sortedDescending
function, which gives the same result as first using sorted
and then reversed
.
If we want to sort elements by one of their properties, we should use sortedBy
, which sorts elements by the value its selector returns. For instance, if we have a list of students and we want to sort them by the semester, we can use sortedBy
with a selector that reads the semester value.
In other words, in sortedBy
, the selector decides what value should be compared when we sort elements. This value needs to be comparable to instances of the same type (implement Comparable<T>
interface).
All Kotlin standard library sorting functions are implemented in such a way, so that equal elements remain in the same order (so we say that a stable sorting algorithm is being used).
sortedBy
also has a descending alternative called sortedByDescending
.
We might use sortedBy
or sortedByDescending
to sort users by their login, news by publication date, or tasks by priority.
The selectors of sortedBy
and sortedByDescending
accept null
, which is considered less than all other values.
It gets more complicated when we need to sort by more than one property. For example, a typical governmental order of people’s names requires sorting them by their surnames, and then people with the same surnames should be sorted by their first names. How can we implement this? Sorting by name first and then by surname would give us the correct result, but would be terribly inefficient. A much better solution is using sortedWith
.
sortedWith
is a function that returns a collection sorted according to a comparator it receives as an argument. The comparator is an object that implements the Comparator
interface.
In many languages, it is popular to make an object that implements a comparator.
We can do that in Kotlin too, but in most cases it is better to use one of the top-level functions from the standard library. For instance, we can use compareBy
to create a comparator that first compares using one selector; then, if it considers two objects equal, it compares values using the next selector. This way, we can make a comparator with multiple sorting selectors, used lexicographically.
sortedBy(selector)
under the hood is justsortedWith(compareBy(selector))
.
sortedWith
and compareBy
can be used for as many selectors as we want, which makes them really universal for complex sorting.
When we need to construct a more complex comparator, we have a variety of standard library functions. We can create a new comparator using:
compareBy
,naturalOrder
(sorts with natural order),reverseOrder
(sorts with the reverse of natural order),nullsFirst
andnullsLast
(both use natural order, but they also place nulls first or last).
Then, when we have a comparator, we can modify it using functions on Comparator
, such as:
then
orthenComparator
, both of which add another comparator that is used when the previous comparator considers elements equal;thenBy
, which compares values using a selector when the previous comparator considers elements equal;reversed
, which reverses the comparator order.
Sorting mutable collections
If you want to sort a mutable collection, you can use the sort
function. This is a part of classic collection processing as it modifies a mutable list instead of returning a processed one. The sort
method is often confused with sorted
. The sort
method is an extension function on MutableList
that, in contrast to sorted
, sorts a list and returns Unit
. The sorted
method is an extension function on Iterable
that does not modify its receiver and returns a sorted collection.
There are also sortBy
, sortByDescending
and sortWith
, which respectively work similarly to sortedBy
, sortedByDescending
and sortedWith
, but they modify a mutable collection instead of returning a new one.
Maximum and minimum
Another common situation is that we need to find extremes in a collection: the biggest or the smallest element. We could first sort the elements and then take the first or the last one, but such a solution would be far from optimal. Instead, we should use functions that start with the "max" or "min" prefix.
If we want to find an extreme using the natural order of the elements, use maxOrNull
or minOrNull
, both of which return null
when a collection is empty.
If we want to find an extreme according to a selector (similar to sortedBy
), use maxByOrNull
or minByOrNull
.
You can also find an extreme according to a comparator. In such a case, use maxWithOrNull
or minWithOrNull
.
Another case is when you want to find an extreme value of a property: not the element that contains the extreme value but the value itself. For example, you have a list of students and you want to find their highest score. You could map the students to scores and then find the maximal value, or you could find the student with the highest score and get this score. However, both of these options do a lot of unnecessary operations. Instead, we should use the maxOfOrNull
or minOfOrNull
method with a selector that extracts a score (or maxOf
/minOf
if you are sure that your collection is not empty).
shuffled
and random
We have learned how to sort elements, but we might also want to shuffle them. To get a random number from a collection, use random
(or randomOrNull
for possibly empty lists). To shuffle an iterable (to make its order random), use shuffled
. For these functions, you can pass a custom Random
object as an argument.