Back to Blog
Java

Java ArrayList Sort: Collections.sort() and Comparator

Learn how to sort an ArrayList in Java using Collections.sort(), Comparators, reverse order, and Stream.sorted, with practical examples and edge cases.

JavaArrayListCollections.sortComparatorJava Streams
Illustration of an ArrayList being sorted with a comparator, showing ordered elements and a sort arrow.

To sort an ArrayList in Java, use Collections.sort(). It sorts the list in place using either the natural ordering of its elements or a Comparator that you provide. This article covers the main sorting techniques, including reverse order, multi-field comparisons, and the stream-based alternative, along with performance and edge-case behavior to keep in mind.

Sorting with Collections.sort()

The simplest way to sort an ArrayList is to call Collections.sort() on the list. This works when the elements implement Comparable, meaning they define a natural order. Common classes like String, Integer, and Double already implement Comparable, so you can sort them directly.

import java.util.ArrayList; import java.util.Collections; ArrayList<String> names = new ArrayList<>(); names.add("Charlie"); names.add("Alice"); names.add("Bob"); Collections.sort(names); System.out.println(names); // [Alice, Bob, Charlie]

The list is modified in place; no new list is returned. The natural ordering for String is lexicographic, and for numeric types it is ascending numeric order. With a generic list, the compiler rejects a one-argument Collections.sort() call when the element type does not implement Comparable. If you use raw types or mix element types without a common natural order, the failure can appear at runtime as a ClassCastException.

Custom Sorting with Comparator

When the natural order is not what you need, pass a Comparator to Collections.sort(). A Comparator defines an ordering independently of the element class. With lambdas, this is concise and readable.

Suppose you have a Person class with a name field and an age field:

class Person { String name; int age; // constructor, getters, etc. }

To sort by age ascending:

ArrayList<Person> people = new ArrayList<>(); // add people... Collections.sort(people, (p1, p2) -> Integer.compare(p1.age, p2.age));

The lambda returns a negative integer, zero, or a positive integer depending on whether p1 is less than, equal to, or greater than p2. Using Integer.compare() avoids overflow issues that can occur with subtraction.

For a single field, you can also use Comparator.comparing():

Collections.sort(people, Comparator.comparing(p -> p.name));

This is often clearer when the comparison key is a property that itself has a natural order.

Sorting in Reverse Order

To sort in descending order, use Collections.reverseOrder() as the comparator. This works for elements with a natural ordering:

Collections.sort(names, Collections.reverseOrder());

For a custom comparator, call .reversed() on it:

Collections.sort(people, Comparator.comparing(p -> p.age).reversed());

Be careful: reversed() returns a new comparator that reverses the original ordering. The original comparator remains unchanged.

Sorting Objects by Multiple Fields

Often you need to sort by one field, then break ties with another. Comparator.comparing() and thenComparing() chain comparators to achieve this.

Collections.sort(people, Comparator.comparing(Person::getLastName) .thenComparing(Person::getFirstName));

This sorts by last name first, and for people with the same last name, by first name. You can chain as many fields as needed, and each step can be reversed individually if necessary.

Using Stream.sorted() for Non-Destructive Sorting

Java 8 introduced the Stream API, which provides a sorted() method. Unlike Collections.sort(), Stream.sorted() does not modify the original list. It returns a new stream with the elements in sorted order, and you then collect that stream into a new List (or another container).

ArrayList<Integer> numbers = new ArrayList<>(); // add numbers... List<Integer> sorted = numbers.stream() .sorted() .collect(Collectors.toList());

You can also pass a comparator to sorted():

List<Person> sortedPeople = people.stream() .sorted(Comparator.comparing(Person::getAge)) .collect(Collectors.toList());

Use the stream approach when you need to keep the original list unchanged, or when you want to chain other stream operations such as filtering before sorting. If you only need to sort the list in place, Collections.sort() is simpler and avoids collecting the result into a new list.

Performance and Stability Considerations

The sorting algorithm used by Collections.sort() on an ArrayList is a stable, adaptive, iterative mergesort (TimSort). It runs in O(n log n) time in the worst case and can run in O(n) on already sorted or nearly sorted data. Stability means that equal elements retain their relative order after sorting, which matters when you sort by multiple fields in separate passes.

Stream sorted() also has O(n log n) worst-case time complexity, but it adds memory overhead when you collect the result into a new list, which can be significant for large lists. In-place sorting with Collections.sort() uses some extra memory for the temporary array used by the sort, but it does not create a second copy of the list. For primitive arrays, Java uses a dual-pivot quicksort, but ArrayList stores objects, so the TimSort behavior applies.

If you are sorting a very large list and memory is a concern, prefer Collections.sort() over Stream.sorted().

Common Pitfalls and Edge Cases

One common mistake is sorting a list that contains null elements. The natural ordering of null is undefined, and most comparators will throw a NullPointerException. You need to handle nulls explicitly, for example with Comparator.nullsFirst() or nullsLast():

Collections.sort(list, Comparator.nullsLast(Comparator.naturalOrder()));

Another pitfall is modifying the list while sorting. Collections.sort() is not thread-safe; if another thread structurally modifies the list during the sort, the behavior is not guaranteed (you may get a ConcurrentModificationException). Stream pipelines also expect the source not to be structurally modified while the stream is being consumed.

Finally, remember that Collections.sort() works on any List, but it is most efficient for an ArrayList because the list can be sorted directly on its internal element array. For a LinkedList, sorting is less efficient because the elements need to be copied into an array and written back. If sorting is a common operation, prefer an ArrayList instead of a LinkedList.

When you need to sort an ArrayList of custom objects, define a Comparator that explicitly states the ordering. Relying on Comparable is fine for natural order, but for domain-specific sorting, a dedicated comparator makes the code more maintainable and less error-prone.

Java ArrayList Sort: Collections.sort(), Comparator, and Stream Examples | RYUSLOG DEV