Skip to content

How to Sort a List in Java: List.sort(), Collections.sort(), Streams

A wooden toy train on lime green: from left to right, six wagons carry ever taller stacks of colored wooden discs – one coral, two yellow, three green, four teal, five blue, and six violet – and the locomotive with red wheels stands on the far right

You sort a list in Java with List.sort(). The method sorts the list itself: with null as the argument, by the natural order of its elements; with a comparator, in any order you like.

The following program sorts four names first in ascending and then in descending order:

List<String> names = new ArrayList<>(List.of("Paul", "Eva", "Ben", "Antonia"));

names.sort(null);
System.out.println(names);

names.sort(Comparator.reverseOrder());
System.out.println(names);
[Antonia, Ben, Eva, Paul]
[Paul, Eva, Ben, Antonia]

Besides List.sort(), there are two other ways: the older method Collections.sort() and Stream.sorted(), which creates a sorted copy and leaves the original list unchanged.

In this article, you will learn:

  • how List.sort(), Collections.sort(), and Stream.sorted() differ,
  • how to sort objects of your own classes by one or more fields,
  • how to sort in descending order and why reversed() after a lambda does not compile,
  • why List.of() and Stream.toList() throw an UnsupportedOperationException when you sort them,
  • how to sort strings regardless of upper and lower case and with accented characters,
  • how to sort a map by its values,
  • what a stable sort is and when sort() reports “Comparison method violates its general contract!”

How to sort arrays is shown in the article Sorting in Java.

The Examples in This Article

You can find the complete code in the GitHub repository sorting-in-java, in the package eu.happycoders.sort.jdk.list. Each section has its own class there with a main() method that produces the output shown.

Most examples sort objects of the following record Customer with a customer number, first name, and last name:

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

A record comes with a constructor, accessor methods, equals(), hashCode(), and toString() – the output lines in this article come from this toString().

The classes in the repository require Java 25 because they use the simplified main() method without String[] args. The listings in the article run on Java 17 and later, except for the example on List.reversed(), which needs Java 21.

Three Ways to Sort a List

The following table shows the three ways to sort a list, the Java version that introduced each of them, whether they sort the list itself or return a new list, and whether you have to specify a comparator:

CallSinceResultComparator
list.sort(c)Java 8sorts the list itselfrequired
(null for the natural order)
Collections.sort(list)
Collections.sort(list, c)
Java 1.2sorts the list itselfoptional
list.stream().sorted()Java 8new list, the original list stays unchangedoptional

All three ways sort stably: elements that the comparator considers equal keep their order. The section Stable Sorting shows what that means.

I recommend List.sort() when the list itself is to be sorted and Stream.sorted() when the original list must remain unchanged – for example, because it is immutable. You do not need Collections.sort() in new code, because since Java 8, the method only calls List.sort().

Sorting a List With List.sort()

List.sort() has been a default method of the List interface since Java 8. It expects a comparator that defines the order of the elements. If you pass null, the method sorts by the natural order of the elements.

Sorting by Natural Order

Every class that implements the Comparable interface has a natural order – in the JDK, for example, String, Integer, and LocalDate. The natural order sorts numbers in ascending order, dates chronologically, and strings by the Unicode values of their characters.

Instead of null, you can pass Comparator.naturalOrder(). The following program sorts a list of numbers this way:

List<Integer> numbers = new ArrayList<>(List.of(4, 8, 5, 9, 2, 3, 1, 7, 6));
numbers.sort(Comparator.naturalOrder());
System.out.println(numbers);
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Comparator.naturalOrder() sorts exactly like null but makes the intent explicit in the code. The following section, Sorting Objects of Your Own Classes, shows a second advantage: for elements without a natural order, the compiler reports the error before the program even runs.

Sorting Objects of Your Own Classes

The record Customer does not implement Comparable. The following program tries to sort a list of Customer objects with sort(null) anyway:

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

customers.sort(null);

Java aborts with a ClassCastException (shortened – the full message also names the module and class loader of both classes):

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

If you pass Comparator.naturalOrder() instead, the call does not even compile:

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

// does not compile – Customer does not implement Comparable
customers.sort(Comparator.naturalOrder());

javac reports, shortened:

error: incompatible types: inference variable T has incompatible bounds
  customers.sort(Comparator.naturalOrder());
           ^
    upper bounds: Comparable<? super T>
    lower bounds: Customer

The signature of naturalOrder() requires a type that implements Comparable. With null, the compiler cannot check that – the error only shows up at runtime.

For Customer, you therefore have to pass a comparator. The easiest way to create one is Comparator.comparing(). The method expects a function that reads the sort key from each element – here the record’s accessor method lastName():

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

customers.sort(Comparator.comparing(Customer::lastName));
customers.forEach(System.out::println);
Customer[id=57299, firstName=Caroline, lastName=Albers]
Customer[id=10503, firstName=Phil, lastName=Gruber]
Customer[id=52118, firstName=Thomas, lastName=Mann]
Customer[id=43423, firstName=Elizabeth, lastName=Mann]
Customer[id=28378, firstName=Marina, lastName=Metz]
Customer[id=61157, firstName=Patrick, lastName=Sonnenberg]

The two entries with the last name Mann are in the order of the original list: Thomas before Elizabeth. This comparator does not sort by first name.

Sorting by Multiple Fields

You append a second sort key with thenComparing(). It only comes into play when the first key is equal. The following program sorts by last name and, for equal last names, by first name:

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

customers.sort(Comparator.comparing(Customer::lastName)
    .thenComparing(Customer::firstName));
customers.forEach(System.out::println);
Customer[id=57299, firstName=Caroline, lastName=Albers]
Customer[id=10503, firstName=Phil, lastName=Gruber]
Customer[id=43423, firstName=Elizabeth, lastName=Mann]
Customer[id=52118, firstName=Thomas, lastName=Mann]
Customer[id=28378, firstName=Marina, lastName=Metz]
Customer[id=61157, firstName=Patrick, lastName=Sonnenberg]

Now Elizabeth Mann comes before Thomas Mann. For more keys, you append more thenComparing() calls.

For fields of type int, there are Comparator.comparingInt() and thenComparingInt(). They compare the values directly, without boxing them into Integer objects.

Sorting Your Own Classes by Natural Order

If Customer is to have a natural order – for example, by customer number – the record must implement the Comparable interface and its compareTo() method:

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

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

With this version of the record, sort(null) and sort(Comparator.naturalOrder()) sort the list by customer number:

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

customers.sort(null);
customers.forEach(System.out::println);
Customer[id=10503, firstName=Phil, lastName=Gruber]
Customer[id=28378, firstName=Marina, lastName=Metz]
Customer[id=43423, firstName=Elizabeth, lastName=Mann]
Customer[id=52118, firstName=Thomas, lastName=Mann]
Customer[id=57299, firstName=Caroline, lastName=Albers]
Customer[id=61157, firstName=Patrick, lastName=Sonnenberg]

A class has exactly one natural order, but you can write as many comparators for it as you like. The article compareTo, Comparable, Comparator – Comparing Objects in Java explains how the two interfaces differ. In the remaining examples, Customer is again the record without Comparable.

Sorting Lists With null Elements

If the list contains null, sort(null) and sort(Comparator.naturalOrder()) throw a NullPointerException, because the natural order cannot compare null with any element. The list in the following example is created via Arrays.asList() because List.of() does not allow null elements:

List<String> names = new ArrayList<>(Arrays.asList("Paul", null, "Eva", "Ben"));
names.sort(null);
Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke "java.lang.Comparable.compareTo(Object)"
    because "a[runHi]" is null

Comparator.nullsFirst() and Comparator.nullsLast() put null at the beginning or end and sort the remaining elements with the comparator you pass to them:

List<String> names = new ArrayList<>(Arrays.asList("Paul", null, "Eva", "Ben"));
names.sort(Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(names);
[Ben, Eva, Paul, null]

Sorting a List in Descending Order

You sort in descending order with a reversed comparator. For the natural order, Comparator.reverseOrder() provides it – the example at the beginning of the article uses it. You reverse any other comparator with reversed(). The following program sorts by customer number, highest first:

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

customers.sort(Comparator.comparingInt(Customer::id).reversed());
customers.forEach(System.out::println);
Customer[id=61157, firstName=Patrick, lastName=Sonnenberg]
Customer[id=57299, firstName=Caroline, lastName=Albers]
Customer[id=52118, firstName=Thomas, lastName=Mann]
Customer[id=43423, firstName=Elizabeth, lastName=Mann]
Customer[id=28378, firstName=Marina, lastName=Metz]
Customer[id=10503, firstName=Phil, lastName=Gruber]

Why reversed() After a Lambda Does Not Compile

If you write the sort key as a lambda instead of a method reference, the same construct no longer compiles:

// does not compile – the type of c is Object here
customers.sort(Comparator.comparing(c -> c.lastName()).reversed());

The javac compiler reports:

error: cannot find symbol
  customers.sort(Comparator.comparing(c -> c.lastName()).reversed());
                                            ^
  symbol:   method lastName()
  location: variable c of type Object

The compiler infers the type of a lambda parameter without a type declaration from the target type, i.e., from the type expected at the position where the lambda appears. Here, however, comparing() is not an argument of sort(), where a Comparator<Customer> is expected, but the expression to the left of the dot before reversed(). There is no target type there, so the compiler uses Object for the type parameter T of comparing() – and Object has no method lastName().

The method reference Customer::lastName names its type itself and therefore does not need a target type. If you stick with the lambda, you have to specify the type of the parameter (Customer c below):

customers.sort(Comparator.comparing((Customer c) -> c.lastName()).reversed());

Sorting Multiple Fields in Different Directions

reversed() at the end of a chain reverses the entire chain, not just the last key. The following program thus sorts by last name and first name in descending order:

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

customers.sort(Comparator.comparing(Customer::lastName)
    .thenComparing(Customer::firstName)
    .reversed());
customers.forEach(System.out::println);
Customer[id=61157, firstName=Patrick, lastName=Sonnenberg]
Customer[id=28378, firstName=Marina, lastName=Metz]
Customer[id=52118, firstName=Thomas, lastName=Mann]
Customer[id=43423, firstName=Elizabeth, lastName=Mann]
Customer[id=10503, firstName=Phil, lastName=Gruber]
Customer[id=57299, firstName=Caroline, lastName=Albers]

If only the first name is to be sorted in descending order, you pass a comparator for the key to thenComparing() as a second argument – here Comparator.reverseOrder():

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

customers.sort(Comparator.comparing(Customer::lastName)
    .thenComparing(Customer::firstName, Comparator.reverseOrder()));
customers.forEach(System.out::println);
Customer[id=57299, firstName=Caroline, lastName=Albers]
Customer[id=10503, firstName=Phil, lastName=Gruber]
Customer[id=52118, firstName=Thomas, lastName=Mann]
Customer[id=43423, firstName=Elizabeth, lastName=Mann]
Customer[id=28378, firstName=Marina, lastName=Metz]
Customer[id=61157, firstName=Patrick, lastName=Sonnenberg]

The last names are in ascending order; of the two entries with the last name Mann, Thomas comes before Elizabeth.

Reading a Sorted List Backwards With reversed()

If a list is already sorted in ascending order, you do not necessarily have to sort it again for descending order. Since Java 21, List.reversed() returns a reversed view of the same list:

List<String> names = new ArrayList<>(List.of("Paul", "Eva", "Ben", "Antonia"));
names.sort(null);

List<String> descending = names.reversed();
System.out.println(descending);
[Paul, Eva, Ben, Antonia]

The view does not copy any elements but reads the original list from the back. The article Sequenced Collections in Java shows what this means for later changes to the list.

Sorting a List With Collections.sort()

Collections.sort() has existed since Java 1.2. Up to and including Java 7, this method was the usual way to sort a list, because List.sort() only came with Java 8. Since then, Collections.sort() only calls List.sort() – as the JDK source code shows:

public static <T extends Comparable<? super T>> void sort(List<T> list) {
  list.sort(null);
}

public static <T> void sort(List<T> list, Comparator<? super T> c) {
  list.sort(c);
}

Both variants therefore sort exactly like List.sort(), the second one with the comparator you pass:

Collections.sort(customers, Comparator.comparing(Customer::lastName));

There is one difference: the variant without a comparator requires T extends Comparable<? super T> in its signature. For a list of Customer objects without Comparable, the compiler therefore reports an error:

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

// does not compile – Customer does not implement Comparable
Collections.sort(customers);

javac reports, shortened:

error: no suitable method found for sort(List<Customer>)
  Collections.sort(customers);
             ^
    method Collections.<T#1>sort(List<T#1>) is not applicable
      (inference variable T#1 has incompatible bounds
        equality constraints: Customer
        upper bounds: Comparable<? super T#1>)

customers.sort(null), on the other hand, only fails at runtime with a ClassCastException. customers.sort(Comparator.naturalOrder()) gives you the same compile-time check as Collections.sort() – the section Sorting Objects of Your Own Classes shows both errors.

Sorting a List With Stream.sorted()

If the original list is to remain unchanged, you sort via a stream. sorted() returns a sorted stream, and toList() collects its elements into a new list:

List<String> names = List.of("Paul", "Eva", "Ben", "Antonia");

List<String> sortedNames = names.stream()
    .sorted()
    .toList();

System.out.println(sortedNames);
System.out.println(names);
[Antonia, Ben, Eva, Paul]
[Paul, Eva, Ben, Antonia]

The first line shows the sorted copy, the second the unchanged original list. The original list names was created with List.of() here and is therefore immutable – names.sort(null) would throw an UnsupportedOperationException. The stream, on the other hand, only reads the list.

With a comparator, you write sorted(comparator):

List<Customer> byLastName = customers.stream()
    .sorted(Comparator.comparing(Customer::lastName))
    .toList();

The stream copies the elements: sorted() collects them in an array of its own, sorts it with the same algorithm as List.sort(), and then passes the elements on to toList(), which builds the new list from them.

Stream.toList() has existed since Java 16, and the list it creates is immutable. Up to Java 15, you had to write collect(Collectors.toList()). That returns an ArrayList in every Java version, but the Javadoc guarantees neither the type nor the mutability.

If you need a list that you can safely modify after sorting, you have to specify its type yourself with Collectors.toCollection() – the Javadoc of Collectors.toList() points to this method, too:

List<String> sortedNames = names.stream()
    .sorted()
    .collect(Collectors.toCollection(ArrayList::new));

sortedNames.add("Zoe");

System.out.println(sortedNames);
[Antonia, Ben, Eva, Paul, Zoe]

Sorting Immutable Lists

List.of(), List.copyOf(), Stream.toList(), and Collections.unmodifiableList() return immutable lists. If you try to sort one of them, sort() throws an UnsupportedOperationException:

List<String> names = List.of("Paul", "Eva", "Ben", "Antonia");
names.sort(null);
Exception in thread "main" java.lang.UnsupportedOperationException
    at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:156)

The exception is thrown before sort() compares a single element – even for an empty list.

If you need the sorted list for further processing, you copy the elements into an ArrayList and sort the copy:

List<String> names = List.of("Paul", "Eva", "Ben", "Antonia");

List<String> sortedNames = new ArrayList<>(names);
sortedNames.sort(null);

System.out.println(sortedNames);
System.out.println(names);
[Antonia, Ben, Eva, Paul]
[Paul, Eva, Ben, Antonia]

If you only need it for reading, Stream.sorted() from the previous section is the shorter way.

Arrays.asList() Sorts the Array Too

Arrays.asList() returns a list whose length is fixed but whose elements you can replace – and thus also sort. The list is a view of the array you passed, not a copy. When you sort the list, you therefore also sort the array:

String[] array = {"Paul", "Eva", "Ben", "Antonia"};
List<String> names = Arrays.asList(array);

names.sort(null);

System.out.println(names);
System.out.println(Arrays.toString(array));
[Antonia, Ben, Eva, Paul]
[Antonia, Ben, Eva, Paul]

The following table shows which JDK lists you can sort with sort(). The row for Collectors.toList() applies to current JDKs, not to every implementation:

List created withSortable
new ArrayList<>(), new LinkedList<>()yes
Arrays.asList()yes
(sorts the array too)
Collectors.toList()yes
(not guaranteed)
Collectors.toCollection(ArrayList::new)yes
List.of(), List.copyOf()no
(UnsupportedOperationException)
Stream.toList()no
(UnsupportedOperationException)
Collections.unmodifiableList()no
(UnsupportedOperationException)

Sorting Strings: Upper and Lower Case and Accented Characters

The natural order of String compares the characters of two strings one after the other by their Unicode values. Uppercase letters (A to Z: 65 to 90) therefore come before lowercase letters (a to z: 97 to 122), and accented letters such as “Ö” (214) come after all letters without accents.

The following program sorts five last names first by natural order, then with String.CASE_INSENSITIVE_ORDER, and finally with a Collator for English:

List<String> names = new ArrayList<>(
    List.of("Zimmermann", "de Vries", "Özdemir", "Becker", "Ohm"));

names.sort(null);
System.out.println(names);

names.sort(String.CASE_INSENSITIVE_ORDER);
System.out.println(names);

names.sort(Collator.getInstance(Locale.ENGLISH));
System.out.println(names);
[Becker, Ohm, Zimmermann, de Vries, Özdemir]
[Becker, de Vries, Ohm, Zimmermann, Özdemir]
[Becker, de Vries, Ohm, Özdemir, Zimmermann]

The natural order puts “de Vries” after “Zimmermann”, because the lowercase “d” has a higher value than any uppercase letter, and “Özdemir” at the end. String.CASE_INSENSITIVE_ORDER compares regardless of upper and lower case and moves “de Vries” to the front – “Özdemir” stays at the end.

Only the Collator from java.text sorts “Özdemir” between “Ohm” and “Zimmermann”, because it compares according to the sorting rules of the language you specify with the Locale. In English, an accented letter counts as the letter without the accent. Only if two words differ in nothing but the accent does the word without it come first: “resume” before “résumé”, but “résumé” before “resumes”. Collator implements Comparator<Object>, so you can pass it directly to sort().

Sorting a Map by Values

A map has no sort() method. To sort its entries by value, you have to copy them into a list with entrySet() and sort the list with Map.Entry.comparingByValue():

Map<String, Integer> scores = Map.of("Paul", 71, "Eva", 82, "Ben", 95, "Antonia", 88);

List<Map.Entry<String, Integer>> entries = new ArrayList<>(scores.entrySet());
entries.sort(Map.Entry.comparingByValue());
System.out.println(entries);
[Paul=71, Eva=82, Antonia=88, Ben=95]

If you need the result as a map again, you collect the sorted entries in a LinkedHashMap. It keeps the order in which the entries were inserted – a HashMap, on the other hand, has no defined order. The following program sorts the entries by value via a stream, highest first:

Map<String, Integer> scores = Map.of("Paul", 71, "Eva", 82, "Ben", 95, "Antonia", 88);

Map<String, Integer> byScore = scores.entrySet().stream()
    .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue,
        (a, b) -> a,
        LinkedHashMap::new));

System.out.println(byScore);
{Ben=95, Antonia=88, Eva=82, Paul=71}

The fourth argument of Collectors.toMap() determines which map is created. The variant with four arguments requires a merge function as its third argument, which decides which value is kept for duplicate keys. Duplicate keys do not occur here because the entries come from a map. You still have to specify the argument; (a, b) -> a would keep the first value but is never called here.

If you want to sort the entries by key, copy the map into a TreeMap:

Map<String, Integer> scores = Map.of("Paul", 71, "Eva", 82, "Ben", 95, "Antonia", 88);

Map<String, Integer> byName = new TreeMap<>(scores);
System.out.println(byName);
{Antonia=88, Ben=95, Eva=82, Paul=71}

Stable Sorting and What Can Go Wrong

Behind List.sort(), Collections.sort(), and Stream.sorted() is the same algorithm: Timsort. Two properties you should know follow from it: stability, and an exception that can signal a faulty comparator.

Stable Sorting

Timsort sorts stably: elements that the comparator considers equal keep their order. The following program first sorts the list by customer number:

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

customers.sort(Comparator.comparingInt(Customer::id));
customers.forEach(System.out::println);
Customer[id=10503, firstName=Phil, lastName=Gruber]
Customer[id=28378, firstName=Marina, lastName=Metz]
Customer[id=43423, firstName=Elizabeth, lastName=Mann]
Customer[id=52118, firstName=Thomas, lastName=Mann]
Customer[id=57299, firstName=Caroline, lastName=Albers]
Customer[id=61157, firstName=Patrick, lastName=Sonnenberg]

It then sorts the list by last name only:

customers.sort(Comparator.comparing(Customer::lastName));
customers.forEach(System.out::println);
Customer[id=57299, firstName=Caroline, lastName=Albers]
Customer[id=10503, firstName=Phil, lastName=Gruber]
Customer[id=43423, firstName=Elizabeth, lastName=Mann]
Customer[id=52118, firstName=Thomas, lastName=Mann]
Customer[id=28378, firstName=Marina, lastName=Metz]
Customer[id=61157, firstName=Patrick, lastName=Sonnenberg]

Elizabeth Mann (43423) comes before Thomas Mann (52118) – in the order of the first sort, not in the order of the original list, in which Thomas came first.

You can use this when a list already has a meaningful order – for example, orders by time of receipt – and you sort it by another attribute, such as status: within each status, the orders remain sorted by time of receipt.

How List.sort() Sorts Internally

An ArrayList sorts its internal array directly. All other lists – LinkedList, for example – sort via the default implementation of List.sort(): it copies the elements into an array, sorts the array, and writes the elements back into the list with a ListIterator. This costs an additional array with one reference per element. An algorithm that sorted directly within a linked list and accessed the elements by index, on the other hand, would take O(n² log n) time, because every access by index costs O(n) in a linked list.

According to the Javadoc, Timsort itself needs temporary storage of up to n/2 references – considerably less for a nearly sorted list. Timsort also sorts a nearly sorted list with about n comparisons instead of n log n.

The section Sorting Algorithms in the Java Development Kit (JDK) of the article “Sorting in Java” shows which algorithm is behind Arrays.sort() for primitive data types.

“Comparison method violates its general contract!”

If Timsort, while merging two sorted runs, gets into a state that would be impossible with a correct comparator, it aborts with an exception. A common mistake is a comparator that does not return 0 for equal elements. The following program sorts 32 numbers with such a comparator:

List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 32; i++) {
  numbers.add(i % 2);
}

// wrong – never returns 0 for equal numbers
numbers.sort((a, b) -> a < b ? -1 : 1);
Exception in thread "main" java.lang.IllegalArgumentException:
    Comparison method violates its general contract!
    at java.base/java.util.TimSort.mergeLo(TimSort.java:781)
    at java.base/java.util.TimSort.mergeAt(TimSort.java:518)

With 31 numbers, the same program runs without an exception. Below 32 elements, Timsort sorts the entire list with binary insertion sort and does not merge any runs – so the state that triggers the exception cannot arise at all.

But even for larger lists, the exception is not guaranteed: with 32 to 34 numbers, the program throws it, with 35 to 63 numbers it does not, with 64 numbers it does again – and above that, sometimes it does and sometimes it does not. A faulty comparator can therefore pass every unit test and only surface in production – with the exception or, without any message at all, as an incorrectly sorted list.

Integer.compare(a, b) compares correctly, and for the natural order, Comparator.naturalOrder() is enough:

numbers.sort(Comparator.naturalOrder());

The option -Djava.util.Arrays.useLegacyMergeSort=true is often recommended as a way out. It switches back to the old merge sort, which never throws this exception – but the comparator remains faulty, and the JDK source code announces that the option will be removed.

Summary

The best way to sort a list is List.sort(): with Comparator.naturalOrder() by natural order, with Comparator.comparing() and thenComparing() by selected fields, and with reversed() or Comparator.reverseOrder() in descending order. Since Java 8, Collections.sort() only calls List.sort().

You use Stream.sorted() when the original list must remain unchanged – for example, because it comes from List.of() or Stream.toList() and sort() would throw an UnsupportedOperationException there. Since Java 21, you can iterate over an ascending sorted list in descending order with reversed() without sorting the list again.

You sort strings with accented characters with a Collator, and a map by values via a list of its entries. And if sort() reports “Comparison method violates its general contract!”, your comparator does not return 0 for equal elements or contradicts itself in some other way.

The article compareTo, Comparable, Comparator – Comparing Objects in Java explains how Comparator and Comparable are related. The article Sorting in Java shows how to sort arrays – including in parallel.

Did this article answer your questions? Then I’d be happy about a review on my ProvenExpert profile – it helps other developers find this content.

👉 Leave a review

Want Even More Knowledge?

My blog features many articles on Java, software architecture, and performance — from foundational concepts to advanced patterns.

If you want to go deeper, check out my trainings: hands-on, easy to understand, and directly applicable to your day-to-day project work. Instead of theory, I teach principles that help you write code that is better, more maintainable, and more performant in the long run.

Explore the Java Trainings

Become a Better Java Developer

My free newsletter keeps you ahead. Modern Java: new versions & features, performance, and JVM insights – once a month.

Search