Zum Inhalt springen

Sortieren in Java: Arrays, Listen, Objekte [Tutorial]

Vorratsgläser mit Kaffeebohnen, daneben eine umgekippte Tasse

Dieses Tutorial erklärt – Schritt für Schritt und mit vielen Code-Beispielen – wie man in Java primitive Datentypen (ints, longs, doubles, etc.) und Objekte beliebiger Klassen sortieren kann.

Im Einzelnen beantwortet der Artikel folgende Fragen:

  • Wie sortiert man in Java Arrays von primitiven Datentypen?
  • Wie sortiert man in Java Arrays und Listen von Objekten?
  • Wie sortiert man absteigend?
  • Wie sortiert man in Java parallel?
  • Welche Sortieralgorithmen verwendet das JDK intern?

Der Artikel ist Teil des Ultimate Guides über Sortieralgorithmen, der einen Überblick über die gängigsten Sortierverfahren und deren Eigenschaften, wie z. B. deren Zeit- und Platzkomplexität, gibt.

Alle Quellcodes dieses Artikels findest du in meinem GitHub-Repository.

Was kann man in Java sortieren?

Die folgenden Datentypen lassen sich mit Java-Bordmitteln sortieren:

  • Arrays von primitiven Datentypen (int[], long[], double[], usw.),
  • Arrays und Listen von Objekten, die das Comparable-Interface implementieren,
  • Arrays und Listen von Objekten beliebiger Klassen, mit Angabe eines Comparators, d. h. eines zusätzlichen Objekts, das das Comparator-Interface implementiert (oder eines entsprechenden Lambdas).

Den genauen Unterschied zwischen Comparable und Comparator erkläre ich im Artikel „Comparator, Comparable, compareTo – Java-Objekte vergleichen“. Dort zeige ich auch, wie man seit Java 8 mit Comparator.comparing() sehr elegant Comparatoren erstellen und aneinanderreihen kann.

Arrays.sort() – primitive Datentypen sortieren

Die Klasse java.util.Arrays stellt Sortiermethoden für alle primitiven Datentypen (außer boolean) bereit:

  • static void sort(byte[] a)
  • static void sort(char[] a)
  • static void sort(double[] a)
  • static void sort(float[] a)
  • static void sort(int[] a)
  • static void sort(long[] a)
  • static void sort(short[] a)

Beispiel: Sortieren eines int-Arrays

Das folgende Beispiel zeigt, wie ein int-Array sortiert und dann auf der Konsole ausgegeben wird:

int[] a = {4, 8, 5, 9, 2, 3, 1, 7, 6};
Arrays.sort(a);
System.out.println(Arrays.toString(a));

Die Ausgabe dieses kurzen Programms lautet:

[1, 2, 3, 4, 5, 6, 7, 8, 9]

Teilbereiche eines Arrays sortieren

Für jeden der o. g. Datentypen (int, long, double, usw.) gibt es eine überladene Methode, die nur einen Teilbereich des Arrays sortiert, z. B.:

  • static void sort(int[] a, int fromIndex, int toIndex)

Das folgende Beispiel sortiert nur die ersten fünf Elemente des Arrays:

int[] a = {4, 8, 5, 9, 2, 3, 1, 7, 6};
Arrays.sort(a, 0, 5);
System.out.println(Arrays.toString(a));

Das Programm gibt Folgendes aus:

[2, 4, 5, 8, 9, 3, 1, 7, 6]

Die ersten fünf Elemente 2, 4, 5, 8, 9 wurden sortiert, die restlichen vier Elemente 3, 1, 7, 6, sind unverändert.

Java-Objekte sortieren

Primitive Datentypen werden nach ihrer natürlichen Ordnung sortiert. Dementsprechend wird unser Beispiel-Array [4, 8, 5, 9, 2, 3, 1, 7, 6] nach dem Sortieren zu [1, 2, 3, 4, 5, 6, 7, 8, 9].

Doch in welcher Reihenfolge werden Objekte sortiert?

Integer- und String-Arrays sortieren

Wie ein Integer- oder String-Array sortiert wird, versteht jede:r Java-Entwickler:in intuitiv:

Integer[] a = {4, 8, 5, 9, 2, 3, 1, 7, 6};
Arrays.sort(a);
System.out.println(Arrays.toString(a));

Auch hier bekommen wir:

[1, 2, 3, 4, 5, 6, 7, 8, 9]

Sortieren wir ein paar Vornamen:

String[] names = {"Susan", "Thomas", "Judith", "Daniel", "Eva", "Ben",
    "Antonia", "Paul"};
Arrays.sort(names);
System.out.println(Arrays.toString(names));

Das Ergebnis lautet – wie erwartet:

[Antonia, Ben, Daniel, Eva, Judith, Paul, Susan, Thomas]

Integer-Objekte werden also offensichtlich genauso wie int-Primitive sortiert. Und Strings alphabetisch.

Objekte eigener Klassen sortieren

Doch wie sortiert man Objekte einer selbst geschriebenen Klasse – einen Customer? Oder eine Invoice?

Probieren wir es aus! Hier zunächst unser Customer-Record:

public record Customer(int id, String firstName, String lastName) {}

Ein Record bringt Konstruktor, Accessor-Methoden, equals(), hashCode() und toString() von Haus aus mit – für ein reines Datenobjekt wie dieses genau das Richtige.

Wir versuchen, ein paar Kunden mit Arrays.sort() zu sortieren:

Customer[] customers = {
  new Customer(43423, "Elizabeth", "Mann"),
  new Customer(10503, "Phil", "Gruber"),
  new Customer(61157, "Patrick", "Sonnenberg"),
  new Customer(28378, "Marina", "Metz"),
  new Customer(57299, "Caroline", "Albers")
};
Arrays.sort(customers);
System.out.println(Arrays.toString(customers));

Diesen Versuch quittiert Java mit folgender Fehlermeldung (gekürzt – aktuelle JDKs hängen noch an, in welchem Modul und Class Loader die beiden Klassen liegen):

Exception in thread "main" java.lang.ClassCastException:
    class eu.happycoders.sorting.Customer cannot be cast to
    class java.lang.Comparable

Java weiß ohne zusätzliche Informationen nicht, wie Customer-Objekte sortiert werden sollen. Wie stellen wir diese Informationen bereit? Das erfährst du im nächsten Kapitel.

Sortieren mit Comparable und Comparator

Die Sortier-Instruktionen können wir auf zwei unterschiedliche Arten bereitstellen:

  1. indem wir den Record Customer das Interface java.lang.Comparable implementieren lassen (so wie von der Fehlermeldung gefordert) oder
  2. indem wir der Arrays.sort()-Methode eine Implementierung des Interfaces java.util.Comparator mitgeben.

Die beiden Varianten werden in den folgenden zwei Abschnitten beschrieben. Einen tieferen Einblick in die Interfaces Comparable und Comparator bietet der Artikel „Comparator, Comparable, compareTo – Java-Objekte vergleichen“.

Sortieren mit Comparable

Das Interface java.lang.Comparable definiert eine einzige Methode:

  • public int compareTo(T o)

Diese wird vom Sortieralgorithmus aufgerufen, um zu prüfen, ob ein Objekt kleiner, gleich oder größer als ein anderes Objekt ist. Je nachdem muss die Methode eine negative Zahl, 0 oder eine positive Zahl zurückliefern.

(Wenn du dir die Quellcodes von Integer und String anschaust, wirst du feststellen, dass beide das Comparable-Interface und die compareTo()-Methode implementieren.)

Wir wollen unsere Kunden nach Kundennummer sortieren. Dazu müssen wir den Customer-Record wie folgt erweitern:

public record Customer(int id, String firstName, String lastName)
    implements Comparable<Customer> {

  @Override
  public int compareTo(Customer o) {
    return this.id < o.id ? -1 : (this.id == o.id ? 0 : 1);
  }
}

Die Funktionsweise aus Perspektive der compareTo()-Methode:

  • Wenn meine Kundennummer kleiner ist als deine, dann gib -1 zurück;
  • wenn unsere Kundennummern gleich sind, gib 0 zurück;
  • ansonsten gib 1 zurück.

Etwas kürzer wird es, wenn man die Methode Integer.compare() verwendet. Diese vergleicht die zwei IDs auf genau die gleiche Art und Weise:

@Override
public int compareTo(Customer o) {
  return Integer.compare(this.id, o.id);
}

Unsere so erweiterten Customer-Objekte können wir nun problemlos sortieren lassen (hier noch mal, damit du nicht scrollen musst, das Customer-Sortier-Beispiel von oben):

Customer[] customers = {
  new Customer(43423, "Elizabeth", "Mann"),
  new Customer(10503, "Phil", "Gruber"),
  new Customer(61157, "Patrick", "Sonnenberg"),
  new Customer(28378, "Marina", "Metz"),
  new Customer(57299, "Caroline", "Albers")
};
Arrays.sort(customers);
System.out.println(Arrays.toString(customers));

Dieses Mal läuft das Programm ohne Fehler durch und gibt Folgendes aus (die Zeilenumbrüche habe ich der Übersicht halber manuell eingefügt):

[Customer[id=10503, firstName=Phil, lastName=Gruber],
 Customer[id=28378, firstName=Marina, lastName=Metz],
 Customer[id=43423, firstName=Elizabeth, lastName=Mann],
 Customer[id=57299, firstName=Caroline, lastName=Albers],
 Customer[id=61157, firstName=Patrick, lastName=Sonnenberg]]

Unsere Kunden sind nun, wie gewünscht, nach Kundennummer sortiert.

Was aber, wenn wir die Kunden für einen anderen Use Case nicht nach Nummern, sondern nach Namen sortieren wollen? Wir können ja compareTo() nur einmal implementieren. Müssen wir uns für immer und ewig auf eine Reihenfolge festlegen?

Hier kommt das Interface Comparator ins Spiel, das ich im nächsten Abschnitt beschreiben werde.

Sortieren mit einem Comparator

Mit der Customer.compareTo()-Methode haben wir die sogenannte „natürliche Ordnung“ der Kunden definiert. Mit dem Interface Comparator können wir beliebig viele weitere Sortierreihenfolgen für eine Klasse definieren.

Analog zur compareTo()-Methode definiert das Comparator-Interface die folgende Methode:

  • int compare(T o1, T o2)

Diese wird aufgerufen, um zu prüfen, ob das Objekt o1 kleiner, gleich oder größer als das Objekt o2 ist. Entsprechend muss auch diese Methode eine negative Zahl, 0 oder eine positive Zahl als Rückgabewert liefern.

Seit Java 8 können wir einen Comparator sehr elegant mit Comparator.comparing() erstellen. Mit folgendem Code können wir die Kunden zunächst nach Nachnamen und dann nach Vornamen sortieren:

Arrays.sort(customers,
    Comparator.comparing(Customer::lastName)
        .thenComparing(Customer::firstName));

Wie du siehst, kann man hier beinahe in natürlicher Sprache aufschreiben, wie die Kunden sortiert werden sollen. Customer::lastName und Customer::firstName sind die Accessor-Methoden, die der Record für seine Komponenten generiert.

Den Comparator können wir auch in einer Konstanten im Customer-Record speichern, um ihn an verschiedenen Stellen wiederzuverwenden:

public static final Comparator<Customer> NAME_COMPARATOR = Comparator
    .comparing(Customer::lastName)
    .thenComparing(Customer::firstName);

Sortieren würden wir die Kunden dann so:

Arrays.sort(customers, Customer.NAME_COMPARATOR);

Weitere Möglichkeiten, um Comparatoren zu erstellen, findest du im Abschnitt „Wie erstellt man einen Comparator?“ des Comparator-Artikels. Probier es einfach mal aus!

Absteigend sortieren

Auch absteigend zu sortieren ist Sache eines Comparators. Comparator.reverseOrder() liefert die umgekehrte natürliche Ordnung – hier also absteigende Kundennummern:

Arrays.sort(customers, Comparator.reverseOrder());

Und jeden Comparator kann man mit reversed() umdrehen:

Arrays.sort(customers, Customer.NAME_COMPARATOR.reversed());

Sortieren einer Liste in Java

Bis jetzt haben wir ausschließlich die folgenden zwei Methoden der Klasse java.util.Arrays verwendet, um Objekte zu sortieren:

  • static void sort(Object[] a) – zum Sortieren von Objekten entsprechend ihrer natürlichen Ordnung,
  • static void sort(T[] a, Comparator<? super T> c) – zum Sortieren von Objekten anhand des übergebenen Comparators.

Oft haben wir Objekte nicht in einem Array vorliegen, sondern in einer Liste. Um diese zu sortieren, gibt es drei Möglichkeiten:

Liste sortieren mit Collections.sort()

Bis einschließlich Java 7 musste die Methode Collections.sort() zu Hilfe genommen werden, um eine Liste zu sortieren.

Im folgenden Beispiel sollen wieder unsere Kunden sortiert werden, zunächst nach Kundennummer (also entsprechend ihrer „natürlichen Ordnung“):

ArrayList<Customer> customers = new ArrayList<>(List.of(
    new Customer(43423, "Elizabeth", "Mann"),
    new Customer(10503, "Phil", "Gruber"),
    new Customer(61157, "Patrick", "Sonnenberg"),
    new Customer(28378, "Marina", "Metz"),
    new Customer(57299, "Caroline", "Albers")
));
Collections.sort(customers);
System.out.println(customers);

Das Programm gibt, wie im vorherigen Beispiel auch, die Kunden sortiert nach ihrer Kundennummer aus.

Collections.sort() prüft übrigens (im Gegensatz zu Arrays.sort()) schon zur Compile-Zeit, ob die übergebene Liste aus Objekten besteht, die Comparable implementieren. Seit Java 8 ist die Methode nur noch eine Weiterleitung an List.sort(), das ich gleich zeige.

Liste sortieren mit Collections.sort() und einem Comparator

Auch einen Comparator kann man Collections.sort() mitgeben. Folgende Code-Zeile sortiert die Kunden nach Namen:

Collections.sort(customers, Customer.NAME_COMPARATOR);

Liste sortieren mit List.sort()

Seit Java 8 gibt es (dank der Default-Methoden in Interfaces) die Möglichkeit, eine Liste direkt mit List.sort() zu sortieren. Dabei muss immer ein Comparator angegeben werden:

customers.sort(Customer.NAME_COMPARATOR);

Der Comparator darf allerdings auch null sein, um die Liste entsprechend ihrer natürlichen Ordnung zu sortieren:

customers.sort(null);

Auch hier bekommen wir eine ClassCastException, wenn die übergebene Liste Objekte enthält, die nicht Comparable implementieren.

Liste sortieren mit Stream.sorted()

Wer die Ausgangsliste nicht verändern will – oder eine unveränderliche Liste wie die von List.of() vorliegen hat –, sortiert über einen Stream:

List<Customer> sorted = customers.stream().sorted().toList();

sorted() ohne Argument verwendet die natürliche Ordnung, sorted(Customer.NAME_COMPARATOR) einen Comparator. Das Ergebnis ist eine neue, wiederum unveränderliche Liste; toList() gibt es seit Java 16, davor schreibt man collect(Collectors.toList()).

Arrays parallel sortieren

Seit Java 8 steht jede der Sortiermethoden aus der java.util.Arrays-Klasse auch in einer parallelen Variante zur Verfügung. Diese verteilt den Sortieraufwand ab einer festgelegten Array-Größe auf mehrere CPU-Kerne: bei primitiven Arrays ab 4.097 Elementen (Java 8 bis 13: ab 8.193), bei Objekt-Arrays ab 8.193 Elementen. Ein Beispiel:

  • static void parallelSort(double[] a)

Das folgende Beispiel misst die benötigte Zeit für das Sortieren von 100 Millionen double-Werten einmal mit Arrays.sort() und einmal mit Arrays.parallelSort():

public class DoubleArrayParallelSortDemo {
  private static final int NUMBER_OF_ELEMENTS = 100_000_000;

  public static void main(String[] args) {
    for (int i = 0; i < 5; i++) {
      sortTest("sort", Arrays::sort);
      sortTest("parallelSort", Arrays::parallelSort);
    }
  }

  private static void sortTest(String methodName, Consumer<double[]> sortMethod) {
    double[] a = createRandomArray(NUMBER_OF_ELEMENTS);
    long time = System.currentTimeMillis();
    sortMethod.accept(a);
    time = System.currentTimeMillis() - time;
    System.out.println(methodName + "() took " + time + " ms");
  }

  private static double[] createRandomArray(int n) {
    ThreadLocalRandom current = ThreadLocalRandom.current();
    double[] a = new double[n];
    for (int i = 0; i < n; i++) {
      a[i] = current.nextDouble();
    }
    return a;
  }
}

Auf einem Dell XPS 17 mit Intel Core i7-12700H (sechs Performance- und acht Efficiency-Kerne) gibt das Programm unter Java 25 folgende Messwerte aus:

sort() took 8408 ms
parallelSort() took 1222 ms
sort() took 8086 ms
parallelSort() took 1311 ms
sort() took 8062 ms
parallelSort() took 1238 ms
sort() took 8000 ms
parallelSort() took 897 ms
sort() took 8045 ms
parallelSort() took 857 ms

Und auf einem Mac mit Apple M5 Pro (18 Kerne), ebenfalls unter Java 25:

sort() took 6408 ms
parallelSort() took 581 ms
sort() took 6470 ms
parallelSort() took 652 ms
sort() took 6540 ms
parallelSort() took 674 ms
sort() took 6592 ms
parallelSort() took 682 ms
sort() took 6573 ms
parallelSort() took 735 ms

Auf dem XPS 17 dauert der erste sort()-Aufruf etwas länger, weil der HotSpot-Compiler den Code erst optimieren muss; auf dem M5 Pro ist kein Warmup-Effekt zu sehen.

Wie viel das parallele Sortieren bringt, hängt an den Kernen – und daran, welche es sind. Hier die Mediane aus je fünf Durchläufen, ergänzt um die Messung aus der ersten Fassung dieses Artikels von 2020:

RechnerKernesort()parallelSort()Faktor
Dell XPS 15, Core i7-8750H (2020)69,2 s1,8 s5,1
Dell XPS 17, Core i7-12700H6 P + 8 E8,1 s1,2 s6,6
Mac, Apple M5 Pro186,5 s0,67 s9,7

Zwei Dinge fallen auf. Erstens skaliert keines der Systeme perfekt: 18 Kerne bringen Faktor 9,7, nicht 18. Der Merge-Schritt am Ende, der Speicherdurchsatz und die Verwaltung der Threads haben ihren Preis.

Zweitens schwankt parallelSort() auf dem XPS 17 zwischen 0,86 und 1,3 Sekunden, während sort() stabil bleibt. Das ist die Signatur der gemischten Kerne: Der ForkJoinPool verteilt die Arbeit auf 19 Worker-Threads, und ob ein Teilstück auf einem Performance- oder einem Efficiency-Kern landet, entscheidet über die Gesamtzeit – fertig ist das Sortieren erst, wenn der langsamste Teil fertig ist. In den besten Durchläufen erreicht der XPS 17 Faktor 9,4, in den schlechtesten 6,1.

Sortieralgorithmen im Java Development Kit (JDK)

Im JDK werden je nach Elementtyp und Array-Größe verschiedene Sortieralgorithmen angewendet. Die folgende Matrix zeigt, welcher Algorithmus hinter Arrays.sort() und Arrays.parallelSort() steckt. Die Schwellwerte stammen aus dem JDK-Quellcode und sind seit Java 14 unverändert (geprüft bis Java 27):

Matrix der Sortieralgorithmen in java.util.Arrays: Counting Sort für byte, short und char oberhalb kleiner Schwellwerte, Dual-Pivot Quicksort für die übrigen primitiven Typen, Timsort für Objekte; parallel wird nur oberhalb von 4.096 bzw. 8.192 Elementen sortiert
Welchen Algorithmus java.util.Arrays verwendet

Sequenziell, also mit Arrays.sort():

  • Counting Sort für byte[] ab 65 Elementen (darunter Insertion Sort) sowie für short[] und char[] ab 1.751 Elementen (darunter Dual-Pivot Quicksort).
  • Dual-Pivot Quicksort für int[], long[], float[] und double[]. Seit Java 14 ist das kein reines Quicksort mehr, sondern ein Hybrid: Teilbereiche unter 44 Elementen sortiert Insertion Sort, bereits sortierte Läufe werden gemergt statt partitioniert, und bei zu tiefer Rekursion schaltet die Methode auf Heapsort um. So bleibt die Zeitkomplexität auch bei Eingaben bei O(n log n), bei denen andere Quicksort-Implementierungen auf O(n²) zurückfallen. Was Arrays.sort() im Einzelnen tut, beschreibt der Abschnitt „Was Arrays.sort() daraus gemacht hat“ im Quicksort-Artikel. Seit Java 22 beschleunigen auf x86-Prozessoren zusätzlich SIMD-Intrinsics (AVX-512) das Partitionieren und das Sortieren kleiner Teilbereiche.
  • Timsort (ein optimiertes Natural Mergesort kombiniert mit Insertion Sort) für alle Objekt-Arrays – und damit auch für Listen. Timsort ist stabil: Elemente, die der Comparator als gleich einstuft, behalten ihre Reihenfolge.

Parallel, also mit Arrays.parallelSort():

  • Bytes, Shorts und Characters werden seit Java 14 nicht mehr parallel sortiert; parallelSort() ruft hier dieselbe sequenzielle Implementierung auf wie sort().
  • int[], long[], float[] und double[] ab 4.097 Elementen: dasselbe Dual-Pivot Quicksort, aber Partitionen und Merge-Schritte laufen als Tasks im Common ForkJoinPool. Darunter wird sequenziell sortiert.
  • Objekt-Arrays ab 8.193 Elementen zerlegt ein paralleles Mergesort in Teilstücke, sortiert diese mit Timsort und mergt sie wieder. Darunter läuft das sequenzielle Timsort, da sonst der Overhead größer wäre als der Gewinn.

Voraussetzung ist in beiden Fällen, dass der Common ForkJoinPool mehr als einen Thread hat – auf einem Einkern-System sortiert parallelSort() immer sequenziell.

Zusammenfassung

Du hast in diesem Artikel gelernt (oder aufgefrischt), wie du in Java primitive Datentypen und Objekte sortieren kannst – aufsteigend, absteigend, in Arrays, Listen und Streams, sequenziell und parallel – und welche Sortierverfahren das JDK intern anwendet. Sortierte Daten sind außerdem die Voraussetzung für einen der schnellsten Suchalgorithmen: die binäre Suche in Java.

War dieser Artikel hilfreich für dich? Dann freue ich mich, wenn du dir kurz Zeit für eine Bewertung auf meinem ProvenExpert-Profil nimmst.

👉 Bewertung abgeben

Wenn du informiert werden möchtest, sobald der nächste Artikel erscheint, klicke hier und trage dich für den HappyCoders-Newsletter ein.

👉 Newsletter-Anmeldung

Datenstrukturen wirklich verstehen?

Dieser Artikel zeigt eine Datenstruktur. „Mastering Data Structures in Java“ zeigt dir alle – und vor allem, wann du welche einsetzt und warum das über die Laufzeit deiner Anwendung entscheidet.

Der Premium-Online-Kurs ist gerade geschlossen. Auf der Warteliste erfährst du als Erste:r, wenn er wieder öffnet – und bekommst das Angebot vor allen anderen.

Zur Warteliste

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche