Sorting objects by a certain property or field is a common task. However, as we begin to define our own classes, we need a framework that allows us to support sorting our objects. The Comparable and Comparator interfaces provide such a framework.
Both interfaces define a comparTo(t : T) : Int method. When one object is greater than another object, compareTo should return a positive number (1 or greator). When two objects are equal, compareTo should return zero. Finally, when one object is less than another object, compareTo should return a negative number (-1 or less).
Since Kotlin supports operator overloading, class that implement the Comparable interface have the added bonus of being able to support comparison operators such as <, <=, >, >=. Operator overloading was added to Kotlin to improve code readability and conciseness. After all, it’s much easier to read str1 < str2 as opposed to str1.compareTo(str2) == 1. Nevertheless, it’s still on the developer to decide how two classes are compared.
It’s typical to use field by field comparison. For example, if we are sorting people, we may choose to sort by last name followed by first name. In other cases, we may wish to sort by employee id number. When our classes implement Comparable, we are specifying a default or natural ordering for our classes. To help us define a compareTo method, we may wish to turn to 3rd party libraries such as Apache Commons or Google Guava which provide excellent tools to implement compareTo. Some IDES can even generate code to implement the compareTo method.
It’s not very difficult to implement compareTo in the absence of tools. All Kotlin primitives support compareTo, so implemeting compareTo can be a straightforward class. Let’s look at this example.
data class FamilyMember(val firstName : String, val lastName : String, val age : Int) : Comparable<FamilyMember> { /** * Having a compareTo method also overloads the comparison operators * >, >=, <, <= */ override fun compareTo(other: FamilyMember): Int { val fName = firstName.compareTo(other.firstName) val lName = lastName.compareTo(other.lastName) return fName.compareTo(lName) } }
In our compareTo example, we compare first names by calling compareTo on both names and store the result in a variable. We do the same for last names. Finally, we call compareTo on the fName and lName variables and return the result. At this point our family class has a natural ordering and may use the comparison operators.
When we wish to sort a collection class, we can use the sort() method. The sort() method calls compareTo on each object held in the collection and sorts the items.
val belchers = mutableListOf( FamilyMember("Bob", "Belcher", 45), FamilyMember("Linda", "Belcher", 44), FamilyMember("Tina", "Belcher", 13), FamilyMember("Gene", "Belcher", 11), FamilyMember("Louise", "Belcher", 9)) belchers.sort() println(belchers)
After calling belchers.sort(), all family members are sorted by last name, first name. However, what if we want to sort by a different property, say age? That’s when we use Comparator.
Since Kotlin has both object expressions and lambda expressions, we don’t tend to define classes that implement Comparator. Instead, since Comparator is a single abstract method (SAM) interface, we typically use Comparator in a lambda expression. Let’s see how we can sort the belchers by their age.
belchers.sortBy { it.age } // This creates a Comparator that compares by age println(belchers)
The FamilyMember.age property is an Int, which already implements Comparable. So when the compiler sees it.age, it has all the information it needs to create a Comparator that compares by age. Since it’s not completely known in advance how we are expected to sort objects, Comparators give developers extra flexibility when we need to sort objects differently from the natural order.
Putting it Together
This is an example program that shows how to use both Comparable and Comparator.
data class FamilyMember(val firstName : String, val lastName : String, val age : Int) : Comparable<FamilyMember> { /** * Having a compareTo method also overloads the comparison operators * >, >=, <, <= */ override fun compareTo(other: FamilyMember): Int { val fName = firstName.compareTo(other.firstName) val lName = lastName.compareTo(other.lastName) return fName.compareTo(lName) } } fun main(args : Array<String>){ val belchers = mutableListOf( FamilyMember("Bob", "Belcher", 45), FamilyMember("Linda", "Belcher", 44), FamilyMember("Tina", "Belcher", 13), FamilyMember("Gene", "Belcher", 11), FamilyMember("Louise", "Belcher", 9)) val bob = belchers[0] val gene = belchers[3] println("Before sort") println(belchers) println() println("Sorting using natural ordering") belchers.sort() println(belchers) println() println("Sorting by age") belchers.sortBy { it.age } println(belchers) println() println("Testing operator overloading") println("Bob > Gene? " + (bob > gene)) }
Output
Before sort [FamilyMember(firstName=Bob, lastName=Belcher, age=45), FamilyMember(firstName=Linda, lastName=Belcher, age=44), FamilyMember(firstName=Tina, lastName=Belcher, age=13), FamilyMember(firstName=Gene, lastName=Belcher, age=11), FamilyMember(firstName=Louise, lastName=Belcher, age=9)] Sorting using natural ordering [FamilyMember(firstName=Bob, lastName=Belcher, age=45), FamilyMember(firstName=Gene, lastName=Belcher, age=11), FamilyMember(firstName=Linda, lastName=Belcher, age=44), FamilyMember(firstName=Louise, lastName=Belcher, age=9), FamilyMember(firstName=Tina, lastName=Belcher, age=13)] Sorting by age [FamilyMember(firstName=Louise, lastName=Belcher, age=9), FamilyMember(firstName=Gene, lastName=Belcher, age=11), FamilyMember(firstName=Tina, lastName=Belcher, age=13), FamilyMember(firstName=Linda, lastName=Belcher, age=44), FamilyMember(firstName=Bob, lastName=Belcher, age=45)] Testing operator overloading Bob > Gene? false