Java Collections.sort(): Natural Order, Comparator, Stability

Java Collections.sort() sorts a List in place. Learn natural order, Comparator and reverseOrder(), stability, List.sort() and the exceptions it throws.

Collections.sort calls List.sort, which copies the elements to an array, sorts it with TimSort and writes them back

The method Collections.sort() sorts a List in place, either in the natural order of its elements or in the order defined by a Comparator that we pass as the second argument. It returns nothing, the sort is stable, and the list must be modifiable.

We call Collections.sort() when a list must stay sorted for the rest of the code, such as a leaderboard, a dropdown of city names, or a list we search with Collections.binarySearch() afterwards.

The following example sorts a list of city names with both overloads and shows what happens with an unmodifiable list.

List<String> cities = new ArrayList<>(List.of("Pune", "Delhi", "Agra", "Mumbai"));
Collections.sort(cities);
List<String> ascending = List.copyOf(cities);                       // [Agra, Delhi, Mumbai, Pune]
Collections.sort(cities, Collections.reverseOrder());
List<String> descending = List.copyOf(cities);                      // [Pune, Mumbai, Delhi, Agra]
Collections.sort(cities, Comparator.comparing(String::length));
List<String> byLength = List.copyOf(cities);                        // [Pune, Agra, Delhi, Mumbai]

Collections.sort(List.of("Pune", "Agra"));                          // UnsupportedOperationException

Notice the byLength result. “Pune” and “Agra” have the same length, and they keep the order they had before that sort, because the sort is stable. In the sections below, we look at what Collections.sort() does inside, the rules for natural order and comparators, stability, which lists it accepts, sorting a part of a list, and how it compares with List.sort() and Stream.sorted().

1. What Collections.sort() Does Inside

The class java.util.Collections has two overloads of sort(). Since Java 8, both are one-line methods that call the default method List.sort() of the list, so the real work happens in the list implementation.

Collections.sort calls List.sort, which copies the elements to an array, sorts it with TimSort and writes them back
Collections.sort() hands the list to List.sort(), and ArrayList overrides it to sort its own array without a copy
OverloadOrderRequirement
sort(List<T> list)Natural order, from compareTo()T implements Comparable, checked by the compiler
sort(List<T> list, Comparator<? super T> c)Order of the comparatorNone for T; a null comparator means natural order

The default List.sort() copies the elements into an array, sorts the array with Arrays.sort() and writes the elements back through a ListIterator. The classes ArrayList and the fixed-size list from Arrays.asList() override sort() and sort their own array in place, without the copy. In both cases, the algorithm is TimSort, a stable merge sort that takes O(n log n) comparisons on random data and about n comparisons on data that is already almost sorted.

// does not compile: JDK source of Collections (excerpt)
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);
}

2. Sorting in Natural Order

The one-argument overload uses the natural order of the elements, which is the order of their compareTo() method. Strings sort in dictionary order by char value, numbers in ascending order, and dates and times from the earliest to the latest.

List<Integer> scores = new ArrayList<>(List.of(70, 95, 40));
Collections.sort(scores);
List<Integer> sortedScores = List.copyOf(scores);         // [40, 70, 95]

List<LocalDate> dates = new ArrayList<>(List.of(LocalDate.of(2026, 5, 1), LocalDate.of(2025, 12, 24)));
Collections.sort(dates);
List<LocalDate> sortedDates = List.copyOf(dates);         // [2025-12-24, 2026-05-01]

For our own classes, we implement the Comparable interface. The following example is a game leaderboard, where each Player record has a name and a score, and the natural order of players is by name.

record Player(String name, int score) implements Comparable<Player> {

    @Override
    public int compareTo(Player other) {
        return name.compareTo(other.name);
    }

    @Override
    public String toString() {
        return name + "=" + score;
    }
}
List<Player> players = new ArrayList<>(List.of(new Player("mia", 80), new Player("alex", 95), new Player("lee", 80)));
Collections.sort(players);
List<Player> byName = List.copyOf(players);               // [alex=95, lee=80, mia=80]

The compiler checks the bound T extends Comparable<? super T>, so passing a list of a class without Comparable fails at compile time, not at runtime.

// does not compile: Badge does not implement Comparable
record Badge(String label) { }
List<Badge> badges = new ArrayList<>(List.of(new Badge("gold"), new Badge("bronze")));
Collections.sort(badges);

A null element breaks the natural order with a NullPointerException, because the sort calls compareTo() on it. The post on sorting with null values shows how Comparator.nullsLast() fixes that.

3. Sorting With a Comparator

The two-argument overload sorts by any order we define in a Comparator, without changing the class. The leaderboard shows the highest score first, and players with the same score are listed by name.

List<Player> players = new ArrayList<>(List.of(new Player("mia", 80), new Player("alex", 95), new Player("lee", 80)));
Comparator<Player> leaderboard = Comparator.comparingInt(Player::score).reversed().thenComparing(Player::name);
Collections.sort(players, leaderboard);
List<Player> ranking = List.copyOf(players);              // [alex=95, lee=80, mia=80]

For a descending order, the class Collections has two helpers. The method Collections.reverseOrder() reverses the natural order, and Collections.reverseOrder(cmp) reverses any comparator, which gives the same result as cmp.reversed().

List<Player> players = new ArrayList<>(List.of(new Player("mia", 80), new Player("alex", 95), new Player("lee", 80)));
Collections.sort(players, Collections.reverseOrder());
List<Player> nameDescending = List.copyOf(players);       // [mia=80, lee=80, alex=95]
Collections.sort(players, Collections.reverseOrder(Comparator.comparingInt(Player::score)));
List<Player> scoreDescending = List.copyOf(players);      // [alex=95, mia=80, lee=80]

Passing null as the comparator is allowed and means natural order, so Collections.sort(list, null) behaves like Collections.sort(list). For more ways to build comparators, including thenComparing() chains, read sorting by multiple fields.

4. Stable Sorting and Why It Matters

A sort is stable when elements that compare as equal keep their relative order. Collections.sort() guarantees stability, so we can sort in several passes, from the least important key to the most important one, and the earlier order survives inside each group.

For example, an admin page lists players by name, and a user clicks the “score” column header. The page sorts the current list by score only, and players with the same score still appear by name, because the second sort keeps their order.

List<Player> players = new ArrayList<>(List.of(new Player("mia", 80), new Player("alex", 95), new Player("lee", 80)));
Collections.sort(players);
Collections.sort(players, Comparator.comparingInt(Player::score));
List<Player> byScoreThenName = List.copyOf(players);      // [lee=80, mia=80, alex=95]

Because Collections.sort() is stable, a click on a second column never shuffles the rows that tie on that column. A primitive array sorted with Arrays.sort(int[]) has no such guarantee, but equal int values cannot be told apart, so stability does not matter there.

5. Which Lists Collections.sort() Accepts

The list must be modifiable, but it does not have to be resizable. The sort only replaces elements with set(), so a fixed-size list works, and an unmodifiable list throws an UnsupportedOperationException.

ListResult of Collections.sort()
ArrayList, LinkedList, CopyOnWriteArrayListSorted in place
Arrays.asList(array)Sorted in place, and the backing array is sorted too
List.of(), List.copyOf(), Stream.toList()UnsupportedOperationException
Collections.unmodifiableList(list)UnsupportedOperationException
String[] array = {"Pune", "Agra", "Delhi"};
List<String> view = Arrays.asList(array);
Collections.sort(view);
String firstInArray = array[0];                           // "Agra"

List<String> fromStream = Stream.of("Pune", "Agra").toList();
Collections.sort(fromStream);                             // UnsupportedOperationException
List<String> copy = new ArrayList<>(fromStream);
Collections.sort(copy);
List<String> sortedCopy = copy;                           // [Agra, Pune]

A LinkedList does not override sort(), so the default List.sort() copies it to an array, sorts the array and writes the elements back in one pass. As a result, the sort never uses the slow index access of the linked list. The copy costs O(n) extra memory, but the sort still runs in O(n log n).

6. Sorting Part of a List With subList()

Collections.sort() has no range parameters, unlike Arrays.sort(). We pass a subList() view instead, because the view writes its changes through to the original list.

List<Integer> laps = new ArrayList<>(List.of(9, 7, 5, 3, 1));
Collections.sort(laps.subList(1, 4));
List<Integer> middleSorted = List.copyOf(laps);           // [9, 3, 5, 7, 1]

The range is from index 1, inclusive, to index 4, exclusive, so the first and the last element stay where they are. A racing app uses the same idea to sort only the laps of the current session inside a longer history list.

7. Collections.sort() vs List.sort() vs Stream.sorted()

Since Java 8, the instance method list.sort(cmp) does the same work as Collections.sort(list, cmp), because the static method calls it. A stream sorts a copy and leaves the source list unchanged.

MethodChanges the sourceReturnsTypical use
Collections.sort(list)YesvoidExisting code, natural order in one short call
list.sort(cmp)YesvoidNew code, any comparator
list.stream().sorted(cmp).toList()NoNew unmodifiable listKeep the source order, or sort inside a pipeline
List<String> cities = new ArrayList<>(List.of("Pune", "Agra", "Delhi"));
List<String> sortedCopy = cities.stream().sorted().toList();   // [Agra, Delhi, Pune]
List<String> unchanged = List.copyOf(cities);                  // [Pune, Agra, Delhi]
cities.sort(null);
List<String> sortedInPlace = cities;                           // [Agra, Delhi, Pune]

We use list.sort() in new code, because it reads as an action on the list and allows the list implementation to sort in the fastest way. Collections.sort() stays correct and appears in a lot of existing code. For an ArrayList walkthrough with both methods, read sorting an ArrayList in ascending and descending order.

8. Searching a Sorted List With binarySearch()

A sorted list allows Collections.binarySearch(), which finds an element in O(log n) comparisons on an ArrayList. The list must be sorted in the same order that the search uses, otherwise the result is undefined.

List<String> cities = new ArrayList<>(List.of("Pune", "Delhi", "Agra", "Mumbai"));
Collections.sort(cities);
int found = Collections.binarySearch(cities, "Mumbai");   // 2
int missing = Collections.binarySearch(cities, "Goa");    // -3

A negative result means the element is missing, and the value is -(insertion point) – 1. “Goa” belongs at index 2, so the method returns -3. When we sort with a comparator, we pass the same comparator to binarySearch(list, key, cmp). We find the other sort methods of the JDK in the Java sorting guide.

9. Collections.sort() FAQs

Most questions about Collections.sort() start with an exception or with an order that looks wrong.

9.1. Is Collections.sort() Stable?

Yes. The Javadoc guarantees that equal elements are not reordered, for both overloads, and section 4 shows a multi-pass sort that depends on it.

9.2. Which Algorithm Does Collections.sort() Use?

Collections.sort() uses TimSort, through List.sort() and Arrays.sort(Object[]). TimSort is a stable merge sort that finds runs of already sorted elements, so it takes O(n log n) time in the worst case and close to O(n) on nearly sorted lists. Older tutorials that call it a plain merge sort describe the JDK 6 implementation.

9.3. Why Do I Get an UnsupportedOperationException?

The list is unmodifiable, such as a list from List.of(), Stream.toList() or Collections.unmodifiableList(). We copy it into an ArrayList and sort the copy, or we use stream().sorted() to get a new sorted list.

9.4. When Does Collections.sort() Throw a ClassCastException?

With generics, the compiler rejects element types that are not Comparable. A ClassCastException still appears when the elements are Comparable but not mutually comparable, for example a raw list that mixes String and Integer values. The same happens with an Object[] sorted by Arrays.sort().

Object[] mixed = {"Agra", 42};
Arrays.sort(mixed);                                       // ClassCastException

9.5. Can Collections.sort() Sort a Set or a Map?

No, it accepts only a List. For a set, we copy it into a list or use a TreeSet. For a map, we sort its entries, as the posts on sorting a Map by keys and sorting a Map by values show.

10. Conclusion

Collections.sort() sorts a modifiable list in place, in natural order or with a Comparator, and returns nothing. Since Java 8, it calls List.sort(), which uses the stable TimSort algorithm.

Stability lets us sort in several passes without losing the earlier order. Fixed-size lists from Arrays.asList() sort fine, unmodifiable lists throw an UnsupportedOperationException, and a subList() view sorts a part of a list. In new code, list.sort() does the same job, and stream().sorted() returns a sorted copy instead.

11. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.