Wie sortiere ich nach mehreren Werten in Kotlin / vergleiche sie?

84

Angenommen, ich habe eine class Foo(val a: String, val b: Int, val c: Date)und möchte eine Liste von Foos basierend auf allen drei Eigenschaften sortieren . Wie würde ich das machen?

Kirill Rakhman
quelle

Antworten:

148

Kotlins stdlib bietet hierfür eine Reihe nützlicher Hilfsmethoden.

Zunächst können Sie einen Komparator mithilfe der compareBy()Methode definieren und an die sortedWith()Erweiterungsmethode übergeben, um eine sortierte Kopie der Liste zu erhalten:

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

Zweitens können Sie die FooImplementierung Comparable<Foo>mit der compareValuesBy()Hilfsmethode durchführen lassen:

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 })
}

Anschließend können Sie die sorted()Erweiterungsmethode ohne Parameter aufrufen , um eine sortierte Kopie der Liste zu erhalten:

val sortedList = list.sorted()

Sortierrichtung

Wenn Sie nach einigen Werten aufsteigend und nach anderen Werten absteigend sortieren müssen, bietet die stdlib auch Funktionen dafür:

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

Leistungsüberlegungen

Die varargVersion von compareValuesByist nicht im Bytecode enthalten, was bedeutet, dass anonyme Klassen für die Lambdas generiert werden. Wenn die Lambdas selbst den Status nicht erfassen, werden Singleton-Instanzen verwendet, anstatt die Lambdas jedes Mal zu instanziieren.

Wie von Paul Woitaschek in den Kommentaren festgestellt , wird beim Vergleich mit mehreren Selektoren jedes Mal ein Array für den Vararg-Aufruf instanziiert. Sie können dies nicht optimieren, indem Sie das Array extrahieren, da es bei jedem Aufruf kopiert wird. Auf der anderen Seite können Sie die Logik in eine statische Komparatorinstanz extrahieren und wiederverwenden:

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
quelle
4
Beachten Sie, dass wenn Sie mehrere Lambdas-Funktionen verwenden (es gibt eine Überladung mit genau einer, die inline ist), diese nicht inline sind . Dies bedeutet, dass bei jedem Aufruf von comapreTo neue Objekte erstellt werden. Um zu verhindern, dass Sie die Selektoren in das Begleitobjekt verschieben können, werden die Selektoren nur einmal zugewiesen. Ich habe hier einen Ausschnitt
Paul Woitaschek
1
@ KirillRakhman Es erstellt Singletons für die Funktionen, weist aber immer noch Arrays zu:ANEWARRAY kotlin/jvm/functions/Function1
Paul Woitaschek
1
Ab Kotlin 1.1.3 compareBymit mehreren Lambdas wird nicht bei jedem compareToAufruf ein neues Array zugewiesen .
Ilya
1
@Ilya Könnten Sie mich auf das relevante Änderungsprotokoll oder andere Informationen für diese Art der Optimierung verweisen?
Kirill Rakhman
0

Wenn Sie in absteigender Reihenfolge sortieren möchten, können Sie die akzeptierte Antwort verwenden:

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

Oder erstellen Sie eine Erweiterungsfunktion wie 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
}

und verwenden : list.sortedWith(compareByDescending ({ it.a }, { it.b }, { it.c })).

CoolMind
quelle
0

Wenn Sie nach mehreren Feldern sortieren müssen und einige Felder nach absteigend und andere nach aufsteigend, können Sie Folgendes verwenden:

YOUR_MUTABLE_LIST.sortedWith(compareBy<YOUR_OBJECT> { it.PARAM_1}.thenByDescending { it.PARAM_2}.thenBy { it.PARAM_3})
Yago Rey Viñas
quelle
1
Dies wird in meiner Antwort behandelt.
Kirill Rakhman