1. Overview

In Object-Oriented programming languages, we need to sort a collection of objects from time to time. And sometimes, we may need specific sorting logic that can sort on multiple fields of the objects. In this tutorial, we’ll be looking at the built-in support from Kotlin for such functionality.

2. Comparable Interface

One straightforward approach uses the Comparable interface. We can define all comparing logic in the compareTo function and leave sorting to Iterable.sort().

For example, let’s define a data class Student that implements the Comparable interface:

data class Student(val name: String, val age: Int, val country: String? = null) : Comparable<Student> {
    override fun compareTo(other: Student): Int {
        return compareValuesBy(this, other, { it.name }, { it.age })
    }
}

As we can see, we will compare two Student objects by the name first and then age.

Let’s verify it with Iterable.sort(). First, let’s prepare some test fixtures:

private val students = listOf(
    Student(name = "C", age = 9),
    Student(name = "A", age = 11, country = "C1"),
    Student(name = "B", age = 10, country = "C2"),
    Student(name = "A", age = 10),
)

Now, let’s test the functionality of our compareTo function:

val studentsSortedByNameAndAge = listOf(
    Student(name = "A", age = 10),
    Student(name = "A", age = 11, country = "C1"),
    Student(name = "B", age = 10, country = "C2"),
    Student(name = "C", age = 9),
)

assertEquals(
    studentsSortedByNameAndAge,
    students.sorted()
)

3. Sort with Kotlin Built-in Functions

Kotlin has many different utilities and helper functions for sorting collections. These utilities are also equipped for dealing with multiple fields at once. Significantly, they come into play when we can’t add the interface to the target class or we want a variety of sorting options.

3.1. Use sortedWith and compareBy

Let’s give it a try with the functions sortedWith and compareBy:

assertEquals(
    studentsSortedByNameAndAge,
    students.sortedWith(compareBy({ it.name }, { it.age }))
)

We can also use the object properties as selectors:

assertEquals(
    studentsSortedByNameAndAge,
    students.sortedWith(compareBy(Student::name, Student::age))
)

3.2. Use the Comparator in Comparable

Since the compareBy function will return a Comparator object, we can use it in the Comparable.compareTo as well:

override fun compareTo(other: Student): Int {
    return compareBy<Student>({ it.name }, { it.age }).compare(this, other)
}

3.3. Sort with Direction

We can also specify sort direction if needed:

assertEquals(
    listOf(
        Student(name = "C", age = 9),
        Student(name = "B", age = 10, country = "C2"),
        Student(name = "A", age = 11, country = "C1"),
        Student(name = "A", age = 10),
    ),
    students.sortedWith(compareByDescending<Student> { it.name }.thenByDescending { it.age })
)

3.4. Sort with Nullable Values

Nullable values are given special treatment in sorting. By default, null values will be put in the front of the result list:

assertEquals(
    listOf(
        Student(name = "A", age = 10),
        Student(name = "C", age = 9),
        Student(name = "A", age = 11, country = "C1"),
        Student(name = "B", age = 10, country = "C2"),
    ),
    students.sortedWith(compareBy<Student> { it.country }.thenBy { it.name })
)

If we want to put the null values at the end of the result list, we can use the built-in function nullsLast:

assertEquals(
    listOf(
        Student(name = "A", age = 11, country = "C1"),
        Student(name = "B", age = 10, country = "C2"),
        Student(name = "A", age = 10),
        Student(name = "C", age = 9),
    ),
    students.sortedWith(compareBy<Student, String?>(nullsLast()) { it.country }.thenBy { it.name })
)

Likewise, there’s function nullsFirst for a scenario like:

assertEquals(
    listOf(
        Student(name = "A", age = 10),
        Student(name = "C", age = 9),
        Student(name = "B", age = 10, country = "C2"),
        Student(name = "A", age = 11, country = "C1"),
    ),
    students.sortedWith(compareBy<Student, String?>(nullsFirst(reverseOrder())) { it.country }.thenBy { it.name })
)

3.5. Use Customized Comparator with comparing Function

We can use a customized Comparator with sortedWith if we need some specific comparing logic. For example:

val defaultCountry = "C11"
assertEquals(
    listOf(
        Student(name = "A", age = 11, country = "C1"),
        Student(name = "A", age = 10),
        Student(name = "C", age = 9),
        Student(name = "B", age = 10, country = "C2"),
    ),
    students.sortedWith(
        comparing<Student?, String?>(
            { it.country },
            { c1, c2 -> (c1 ?: defaultCountry).compareTo(c2 ?: defaultCountry) }
        ).thenComparing(
            { it.age },
            { a1, a2 -> (a1 % 10).compareTo(a2 % 10) }
        )
    )
)

4. Sort Mutable Collection with sortWith Function

As we saw in previous examples, sortedWith will return a new list object since the original students collection is immutable. If we have a mutable collection and want to sort it in place, we can use the sortWith function:

val mutableStudents = students.toMutableList()
mutableStudents.sortWith(compareBy(Student::name, Student::age))
assertEquals(
    studentsSortedByNameAndAge,
    mutableStudents
)

Of course, we could use sortWith with functions compareByDescending, nullsLast, nullsFirst, and comparing in the same way as we described in section 3.

5. Conclusion

In this article, we explored how we could sort a collection of objects on multiple fields in Kotlin. Specifically, we learned that we can use the Comparable interface and the Kotlin helper functions sortedWith, sortWith, comparing, and compareBy. As always, all these examples and code snippets can be found over on GitHub.