Back to Blog
Java

Using java comparator comparing to Sort Collections

java comparator comparing: Learn how to use Comparator.comparing to sort Java collections by object fields, chain comparators, handle nulls, and avoid common pitfalls.

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

java comparator comparing requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you need to sort a list of objects by one of their fields, Comparator.comparing is the method that turns a key extractor into a Comparator without requiring you to write a full anonymous class. 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.

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 using their natural ordering. This is the most common form of java comparator comparing usage: a clean, declarative 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 handles it, but you should be aware of the allocation cost if the list is large and the sort is performed frequently.

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 this works without explicit type arguments.

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 different 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 is a separate concern. Comparator.comparing throws a NullPointerException if the key extractor returns null for any element. To allow null keys, use Comparator.nullsFirst or Comparator.nullsLast to wrap the comparator.

users.sort(Comparator.nullsLast(Comparator.comparing(User::getName)));

This places users with a null name at the end of the list. For nulls at the beginning, use nullsFirst. These wrappers work with any comparator, not just those produced by comparing.

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) -> 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

The key extractor is invoked once per element during sorting, not once per comparison. The Comparator.comparing implementation caches the extracted keys internally, so the function is not called repeatedly during the sort algorithm. This is a meaningful performance advantage over writing a comparator that calls the getter inside each comparison.

However, if the key extractor is expensive, such as a database lookup or a complex calculation, you should consider extracting the keys into a separate list or using a Map to avoid repeated computation. The caching only applies within a single sort operation; if you sort the same list multiple times, the extractor runs again each time.

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.

When the Key Extractor Returns a Primitive

If the field is a primitive int, long, or double, the autoboxing to Integer, Long, or Double introduces allocation overhead. For very large collections, this can become noticeable. Java 8 does not provide primitive-specialized versions of comparing, so the boxing is unavoidable. If you are sorting millions of records and profiling shows this to be a bottleneck, you can write a custom comparator that compares primitives directly.

users.sort((u1, u2) -> Integer.compare(u1.getAge(), u2.getAge()));

This avoids creating Integer objects for each key. The tradeoff is verbosity and the loss of method-reference readability. In most applications, the boxing cost is negligible compared to the overall sort, but it is worth knowing when the optimization is justified.

Another compatibility note: 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 java 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