Back to Blog
Java

Java Custom Sorting with Comparator and Comparable

Learn how to implement java custom sorting with Comparable, Comparator, lambda expressions, and chained comparators for multi-level sort orders.

ComparatorComparableLambda ExpressionsCollectionsSorting
Illustration of Java custom sorting showing comparator chains organizing employee records by multiple fields.

When you call Collections.sort(list) or list.sort(null) on a list of objects, Java relies on the element type's natural ordering. For primitives and wrapper types like Integer or String, that works fine. But for your own domain objects—an Employee, an Order, a Product—there is no natural order defined. You need java custom sorting to tell the runtime how one instance compares to another.

The java.util package gives you two mechanisms for this: the Comparable interface, which defines a type's natural ordering, and the Comparator interface, which lets you define one or more external orderings without touching the domain class.

Comparable: Embedding the Natural Order

Comparable<T> declares a single method, compareTo(T other), which returns a negative integer, zero, or a positive integer depending on whether this is less than, equal to, or greater than other. Implementing it gives the class a default order that Collections.sort, Arrays.sort, and sorted collections like TreeSet will use automatically.

public class Employee implements Comparable<Employee> { private final String name; private final int salary; public Employee(String name, int salary) { this.name = name; this.salary = salary; } public String getName() { return name; } public int getSalary() { return salary; } @Override public int compareTo(Employee other) { return Integer.compare(this.salary, other.salary); } }

Using Integer.compare instead than subtraction (this.salary - other.salary) avoids integer overflow when salaries are near Integer.MAX_VALUE. The same rule applies to Long.compare and Double.compare for their respective types. A List<Employee> can now be sorted with a single call:

List<Employee> employees = getEmployees(); employees.sort(null); // uses natural ordering

The natural order is now salary ascending. If you later need a different order—by name, by hire date, by department—you have a problem. Comparable gives you exactly one ordering per class. That's where Comparator comes in.

Comparator: Externalizing Sort Logic

Comparator<T> is a functional interface with a single abstract method, compare(T a, T b), which returns the same kind of negative/zero/positive result as compareTo. Because the logic lives outside the domain class, you can define as many orderings as you need without modifying the class.

Comparator<Employee> byName = new Comparator<Employee>() { @Override public int compare(Employee a, Employee b) { return a.getName().compareTo(b.getName()); } }; employees.sort(byName);

The anonymous class form is verbose, but it makes the contract explicit. In practice, you rarely write it that way. Since Comparator is a functional interface, lambda expressions and method references give you the same behavior with far less ceremony.

Lambda Expressions and Method References

The lambda form reduces the comparator to a single expression:

employees.sort((a, b) -> a.getName().compareTo(b.getName()));

For ascending order on a Comparable field, Comparator.comparing is even more direct:

employees.sort(Comparator.comparing(Employee::getName));

n Comparator.comparing accepts a key extractor function and builds a comparator that extracts the key from each element and compares the keys using their natural ordering. For numeric fields, use the specialized factories to avoid boxing overhead:

employees.sort(Comparator.comparingInt(Employee::getSalary)); employees.sort(Comparator.comparingLong(Employee::getHireDateEpoch)); employees.sort(Comparator.comparingDouble(Employee::getRating));

These variants (comparingInt, comparingLong, comparingDouble) work on primitive fields directly and avoid the allocation cost of boxing each key into a wrapper object. On large collections, that difference is measurable.

Chaining Comparators for Multi-Level Sorting

A single field is rarely enough. You often want to sort by salary first, then by name within the same salary group. Comparator.thenComparing composes comparators so that the second one is only consulted when the first returns zero.

Comparator<Employee> bySalaryThenName = Comparator.comparingInt(Employee::getSalary) .thenComparing(Employee::getName); employees.sort(bySalaryThenName);

The chain can be arbitrarily long:

employees.sort( Comparator.comparing(Employee::getDepartment) .thenComparingInt(Employee::getSalary) .thenComparing(Employee::getName) );

Each thenComparing call returns a new comparator that delegates to the previous one and falls through to the next only on ties. This is the idiomatic way to build multi-key sort orders in modern Java, and it reads almost like a declarative specification of the ordering.

Handling Null Values in Custom Sorting

Null elements or null fields are a common source of NullPointerException during sorting. A comparator like Comparator.comparing(Employee::getName) will throw as soon as it encounters an employee whose name is null, because the key extractor returns null and the natural-order comparison on null fails.

Comparator provides two static helpers for this: nullsFirst and nullsLast. Both wrap an existing comparator and define where null keys should appear.

Comparator<Employee> byNameNullsLast = Comparator.comparing(Employee::getName, Comparator.nullsLast(String::compareTo));

Here the second argument to comparing is a comparator for the extracted keys themselves. nullsLast treats a null key as greater than any non-null key, so employees with null names sort to the end. nullsFirst does the reverse.

If the list itself can contain null elements, wrap the whole comparator:

Comparator<Employee> nullSafe = Comparator.nullsLast(byNameNullsLast); employees.sort(nullSafe);

This handles both null elements and null fields in one expression. Without these helpers, you would need to write manual null checks inside every comparator, which quickly becomes repetitive and error-prone across multiple sort sites.

Performance and Stability Considerations

List.sort and Collections.sort use a stable, adaptive merge sort (TimSort) under the hood. Stability matters when you chain comparators: if two elements compare equal under the primary key, their relative order from the original list is preserved. That is why thenComparing works correctly—the secondary comparator only sees elements that were tied, and stability keeps the pre-existing order intact for elements that are equal under every comparator in the chain.

The cost of a custom comparator depends on how expensive the key extraction is. If the key extractor performs a costly computation—parsing a string, dereferencing a nested object graph, or calling a remote service—that cost is paid for every comparison, roughly O(n log n) times. For repeatedly sorted collections, consider precomputing the sort key once per element and sorting on the extracted key instead.

The primitive-specialized factories (comparingInt, comparingLong, comparingDouble) avoid boxing allocation during comparison. For collections of tens of thousands of elements, this reduces garbage collection pressure noticeably. For small lists, the difference is negligible, and readability should win.

Common Pitfalls and Edge Cases

Subtraction-based comparators are the most common bug. return a.getSalary() - b.getSalary() overflows silently when the difference exceeds Integer.MAX_VALUE, producing a wrong ordering. Always use Integer.compare, Long.compare, or the Comparator.comparingInt factory.

Another pitfall is mutating the sort key after the element is placed in a sorted collection. A TreeSet or TreeMap that uses a comparator does not re-sort when a field changes. If an employee's salary changes after insertion, the tree's internal ordering is stale, and subsequent lookups and iterations may behave incorrectly. The safe pattern is to remove the element, mutate it, and reinsert it.

Finally, remember that Comparator and Comparable contracts require consistency with equals. If compareTo returns zero for two objects that are not equals, sorted collections like TreeSet will treat them as duplicates and drop one. If the ordering is inconsistent with equals, the behavior of sorted collections becomes undefined. When you implement a custom comparator, verify that it agrees with the equality semantics your collection relies on.

java custom sorting: Practical Usage and Code Examples | RYUSLOG DEV