99

Say I have a class Foo(val a: String, val b: Int, val c: Date) and I want to sort a list of Foos based on all three properties. How would I go about this?

Kirill Rakhman
  • 42,195
  • 18
  • 124
  • 148

3 Answers3

183

Kotlin's stdlib offers a number of useful helper methods for this.

First, you can define a comparator using the compareBy() method and pass it to the sortedWith() extension method to receive a sorted copy of the list:

val list: List<Foo> = ...
val sortedList = list.sortedWith(compareBy({ it.a }, { it.b }, { it.c }))

Second, you can let Foo implement Comparable<Foo> using the compareValuesBy() helper method:

class Foo(val a: String, val b: Int, val c: Date) : Comparable<Foo> {
    override fun compareTo(other: Foo)
            = compareValuesBy(this, other, { it.a }, { it.b }, { it.c })
}

Then you can call the sorted() extension method without parameters to receive a sorted copy of the list:

val sortedList = list.sorted()

Sorting direction

If you need to sort ascending on some values and descending on other values, the stdlib also offers functions for that:

list.sortedWith(compareBy<Foo> { it.a }.thenByDescending { it.b }.thenBy { it.c })

Performance considerations

The vararg version of compareValuesBy is not inlined in the bytecode meaning anonymous classes will be generated for the lambdas. However, if the lambdas themselves don't capture state, singleton instances will be used instead of instantiating the lambdas everytime.

As noted by Paul Woitaschek in the comments, comparing with multiple selectors will instantiate an array for the vararg call everytime. You can't optimize this by extracting the array as it will be copied on every call. What you can do, on the other hand, is extract the logic into a static comparator instance and reuse it:

class Foo(val a: String, val b: Int, val c: Date) : Comparable<Foo> {

    override fun compareTo(other: Foo) = comparator.compare(this, other)

    companion object {
        // using the method reference syntax as an alternative to lambdas
        val comparator = compareBy(Foo::a, Foo::b, Foo::c)
    }
}
Kirill Rakhman
  • 42,195
  • 18
  • 124
  • 148
  • 5
    Note that if you are using multiple lambdas functions (there is an overload with exactly one which inlines), they are **not inlined**. Which means each call to comapreTo creates a n new objects. To prevent that you could move the selectors to the companion object so the selectors are only allocated once. I created a snipped here: https://gist.github.com/PaulWoitaschek/7f3c4d5310a66ed4984785ee2d6f70ed – Paul Woitaschek Jan 19 '17 at 16:52
  • @PaulWoitaschek if the lambdas don't capture stat, they are compiled to singletons. – Kirill Rakhman Jan 20 '17 at 09:55
  • 1
    @KirillRakhman It creates singletons for the functions but still allocates arrays: `ANEWARRAY kotlin/jvm/functions/Function1` – Paul Woitaschek Jan 20 '17 at 13:08
  • @PaulWoitaschek I've looked at the bytecode and it seems your version allocates an array anyway, because the spread operator will use `Arrays.copyOf`. – Kirill Rakhman Feb 06 '17 at 09:17
  • 1
    Starting with Kotlin 1.1.3 `compareBy` with multiple lambdas won't allocate new array on every `compareTo` call. – Ilya Jun 07 '17 at 14:57
  • 1
    @Ilya could you point me to the relevant changelog or other info for this kind of optimization? – Kirill Rakhman Sep 11 '17 at 09:14
  • 1
    @KirillRakhman https://github.com/JetBrains/kotlin/commit/6a15981b3ac5215af5c68eceaf97c1b735caa8e4 – Ilya Sep 11 '17 at 11:18
  • @KirillRakhman What about sorting multiple values with same comparator? For example CASE_INSENSITIVE_ORDER)? – K.Os Sep 11 '18 at 12:47
  • interesting, this is basically the same as [naturalOrdering](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.comparisons/natural-order.html), although I like this solution better. – lasec0203 May 25 '21 at 02:26
2

If you need sort by multiple fields, and some field by descending and others by ascending you could use:

YOUR_MUTABLE_LIST.sortedWith(compareBy<YOUR_OBJECT> { it.PARAM_1}.thenByDescending { it.PARAM_2}.thenBy { it.PARAM_3})
Yago Rey
  • 209
  • 2
  • 8
1

If you want to sort in descending order, you can use the accepted answer:

list.sortedWith(compareByDescending<Foo> { it.a }.thenByDescending { it.b }.thenByDescending { it.c })

Or create an extension function like compareBy:

/**
 * Similar to
 * public fun <T> compareBy(vararg selectors: (T) -> Comparable<*>?): Comparator<T>
 *
 * but in descending order.
 */
public fun <T> compareByDescending(vararg selectors: (T) -> Comparable<*>?): Comparator<T> {
    require(selectors.size > 0)
    return Comparator { b, a -> compareValuesByImpl(a, b, selectors) }
}

private fun <T> compareValuesByImpl(a: T, b: T, selectors: Array<out (T) -> Comparable<*>?>): Int {
    for (fn in selectors) {
        val v1 = fn(a)
        val v2 = fn(b)
        val diff = compareValues(v1, v2)
        if (diff != 0) return diff
    }
    return 0
}

and use: list.sortedWith(compareByDescending ({ it.a }, { it.b }, { it.c })).

CoolMind
  • 26,736
  • 15
  • 188
  • 224