Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 19, 2024
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.
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()
)
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.
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))
)
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)
}
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 })
)
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 })
)
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) }
)
)
)
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.