Back to Blog
Java

Using Java's Comparator.comparing to Sort Collections

Learn how to use Java's Comparator.comparing to sort collections by object fields, chain comparators, handle null values, and avoid common pitfalls.

JavaComparatorSortingStream APIMethod References
Illustration of two Java objects being compared by a field with a sorting arrow and comparator symbol

When you need to sort a list of objects by one of their fields, Java's Comparator.comparing is the standard way to create a Comparator from a key extractor. It is a static factory method on java.util.Comparator that accepts a Function mapping an object to a Comparable key, and returns a comparator that compares those keys. You no longer need to write an anonymous comparator class for a simple getter-based sort.

List<User> users = getUsers(); users.sort(Comparator.comparing(User::getName));

The method reference User::getName is the key extractor. The resulting comparator compares two User instances by calling getName() on each and comparing the returned String values with their natural ordering. This is the most concise way to sort by a single field.

Basic Usage with Method References

The simplest case is sorting by a field whose type already implements Comparable, such as String, Integer, or LocalDate. You pass a method reference or a lambda that returns that field.

users.sort(Comparator.comparing(User::getAge));

This sorts the list in ascending order of age. The key extractor must return a Comparable type; if it returns a primitive like int, autoboxing converts it to Integer. For primitive int, long, and double fields, prefer the specialized factory methods to avoid boxing:

users.sort(Comparator.comparingInt(User::getAge)); users.sort(Comparator.comparingLong(User::getTimestamp)); users.sort(Comparator.comparingDouble(User::getScore));

You can also use a lambda when the field is not a simple getter:

users.sort(Comparator.comparing(user -> user.getProfile().getCreationDate()));

The lambda is the key extractor. The compiler infers the type of user from the list element type, so explicit type arguments are usually unnecessary.

Chaining Comparators with thenComparing

Real-world sorting rarely stops at one field. When two users have the same age, you might want to order by name. Comparator.comparing returns a comparator that can be extended with thenComparing to create a secondary sort key.

users.sort(Comparator.comparing(User::getAge) .thenComparing(User::getName));

This sorts by age first, and for equal ages, by name. You can chain as many thenComparing calls as needed. Each call takes either another key extractor or an existing comparator, allowing you to mix field types and custom orderings.

users.sort(Comparator.comparing(User::getAge) .thenComparing(User::getName) .thenComparing(User::getId, Comparator.reverseOrder()));

Here the third thenComparing accepts a key extractor and a comparator for that key. This is useful when the natural ordering of the field is not what you want, for example sorting IDs in descending order within the same age and name group.

Reversing Order and Handling Nulls

To sort in descending order, call reversed() on the comparator. This reverses the entire comparator chain, not just the primary key.

users.sort(Comparator.comparing(User::getAge).reversed());

If you need only the primary key reversed but keep secondary keys in their original direction, apply reversed() to the primary comparator before chaining:

users.sort(Comparator.comparing(User::getAge, Comparator.reverseOrder()) .thenComparing(User::getName));

Null handling needs a little more care. Comparator.comparing throws a NullPointerException if the key extractor returns null for any element. That means the outer nullsFirst/nullsLast wrappers only help when the object itself can be null, not when the extracted field is null.

// Null User elements are placed at the end. users.sort(Comparator.nullsLast(Comparator.comparing(User::getName))); // Non-null elements are compared by name; null names are placed last. users.sort(Comparator.comparing(User::getName, Comparator.nullsLast(Comparator.naturalOrder())));

For null names at the beginning, use Comparator.nullsFirst(Comparator.naturalOrder()) as the key comparator.

Type Inference and Common Pitfalls

One frequent issue is the compiler's inability to infer the type of the key extractor when the target type is not obvious. For example, if you sort a Stream and collect the result, the comparator type may need to be specified explicitly.

List<User> sorted = users.stream() .sorted(Comparator.comparing(user -> user.getAge())) .collect(Collectors.toList());

This usually compiles, but if User has overloaded methods or the lambda body is ambiguous, you may need to provide an explicit type witness:

Comparator.<User, Integer>comparing(user -> user.getAge())

Another pitfall is comparing fields that are not Comparable. If the field type does not implement Comparable, you must supply a comparator as the second argument to comparing.

users.sort(Comparator.comparing(User::getRole, (r1, r2) -> Integer.compare(r1.getRank(), r2.getRank())));

This is common when sorting by custom enums that do not follow their declaration order or by objects that have no natural ordering.

Performance and Maintainability Considerations

A comparator returned by Comparator.comparing calls the key extractor each time it compares two elements. The built-in sort algorithm is comparison-based, so the same element may be extracted more than once over the course of a sort. This is usually fine for simple getters, but if the key extractor is expensive, such as a database lookup or a complex calculation, consider extracting the keys into a separate list or using a Map to avoid repeated computation. If you sort the same list multiple times, the extractor runs again each time.

For primitive fields, use the specialized comparingInt, comparingLong, and comparingDouble methods so the comparison works on primitive values instead of boxed objects:

users.sort(Comparator.comparingInt(User::getAge));

You can still write a custom comparator that calls Integer.compare directly, but the factory methods are usually clearer and just as efficient.

From a maintainability perspective, storing a comparator in a static final field is a good practice when the same sort order is used in multiple places.

private static final Comparator<User> BY_AGE_THEN_NAME = Comparator.comparing(User::getAge) .thenComparing(User::getName);

This avoids recreating the comparator on every call and makes the sort order explicit and testable. It also allows you to reuse the comparator in stream operations, TreeSet constructors, or Collections.sort calls without duplication.

Compatibility

Comparator.comparing was introduced in Java 8. If you are working in an older codebase, you must use an anonymous Comparator or a third-party library. Since Java 8 is now the baseline for most projects, this is rarely a constraint, but it matters if you are maintaining legacy code.

The key to using Comparator.comparing effectively is to keep the key extractor simple and side-effect free. A pure function that returns a stable value ensures the comparator behaves consistently across multiple sorts and does not introduce subtle bugs when the underlying object mutates during sorting.

Java Comparator.comparing: Sort Collections by Field | RYUSLOG DEV