Java provides two mechanisms for ordering objects: Comparable (natural ordering built into the class) and Comparator (external, flexible ordering strategies). Understanding both is essential for sorting collections, using TreeSet/TreeMap, and processing ordered streams.

Comparable — Natural Ordering

Implement Comparable<T> to define the default sort order for a class:

  public class Person implements Comparable<Person> {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() { return name; }
    public int age() { return age; }

    @Override
    public int compareTo(Person other) {
        return this.name.compareTo(other.name); // sort by name alphabetically
    }
}

List<Person> people = new ArrayList<>(List.of(
    new Person("Charlie", 30),
    new Person("Alice", 25),
    new Person("Bob", 35)
));

Collections.sort(people); // uses compareTo — Alice, Bob, Charlie
people.stream().sorted().forEach(System.out::println);
  

compareTo Contract

The compareTo method must return:

  • Negative integer if this is less than the argument
  • Zero if they are equal (by sort order)
  • Positive integer if this is greater than the argument

Additional requirements:

  • Transitive: if a > b and b > c, then a > c
  • Consistent with equals: if compareTo returns 0, equals should return true (recommended for TreeSet/TreeMap)
  @Override
public int compareTo(Person other) {
    int nameCompare = this.name.compareTo(other.name);
    if (nameCompare != 0) return nameCompare;
    return Integer.compare(this.age, other.age); // tie-break by age
}
  

Violating the contract causes unpredictable behavior in sorted collections.

Comparator — Custom Ordering

Use Comparator when you need multiple sort orders or cannot modify the class:

  // Sort by age
Comparator<Person> byAge = Comparator.comparingInt(Person::age);
people.sort(byAge);

// Sort by name, then by age
Comparator<Person> byNameThenAge = Comparator
    .comparing(Person::name)
    .thenComparingInt(Person::age);

// Reverse order
people.sort(byAge.reversed());

// Lambda comparator
people.sort((a, b) -> Integer.compare(a.age(), b.age()));
  

Comparator Factory Methods

  Comparator<String> byLength = Comparator.comparingInt(String::length);
Comparator<String> natural = Comparator.naturalOrder();
Comparator<String> reverse = Comparator.reverseOrder();

// Null-safe sorting
Comparator<String> nullsFirst = Comparator.nullsFirst(Comparator.naturalOrder());
Comparator<String> nullsLast = Comparator.nullsLast(Comparator.naturalOrder());

// Extract key then compare
Comparator<Person> byNameLength = Comparator.comparing(p -> p.name().length());
  

Comparable vs Comparator

Feature Comparable Comparator
Package java.lang java.util
Method compareTo(T o) compare(T o1, T o2)
Location Inside the class Separate class, lambda, or method reference
Sort orders One (natural) Multiple independent orders
Modifies class Yes — implements interface No — external to class

Sorting Collections and Arrays

  // List sorting
List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob"));
names.sort(String::compareToIgnoreCase);

// Array sorting
String[] arr = {"Charlie", "Alice", "Bob"};
Arrays.sort(arr, String.CASE_INSENSITIVE_ORDER);

// TreeSet / TreeMap — require ordering
TreeSet<Person> byAgeSet = new TreeSet<>(Comparator.comparingInt(Person::age));
TreeMap<Person, String> byNameMap = new TreeMap<>(byNameThenAge);
byAgeSet.add(new Person("Alice", 25));
byAgeSet.add(new Person("Bob", 35));
  

TreeSet and TreeMap use ordering for both sorting and uniqueness — two elements that compare as equal are considered duplicates in a TreeSet.

Sorting with Streams

  List<Person> sortedByAge = people.stream()
    .sorted(Comparator.comparingInt(Person::age))
    .toList();

// Top 3 oldest
List<Person> top3 = people.stream()
    .sorted(Comparator.comparingInt(Person::age).reversed())
    .limit(3)
    .toList();

// min/max with comparator
Person youngest = Collections.min(people, Comparator.comparingInt(Person::age));
Person oldest = Collections.max(people, Comparator.comparingInt(Person::age));
  

Records and Comparable

Records can implement Comparable directly:

  public record Product(String name, BigDecimal price) implements Comparable<Product> {
    @Override
    public int compareTo(Product other) {
        return this.price.compareTo(other.price);
    }
}
  

Or use external comparators without modifying the record:

  products.sort(Comparator.comparing(Product::price));
  

Practical Example: Multi-Field Sort

  public record Employee(String department, String name, int salary) { }

List<Employee> staff = new ArrayList<>(/* ... */);

staff.sort(Comparator
    .comparing(Employee::department)
    .thenComparing(Employee::name)
    .thenComparing(Employee::salary, Comparator.reverseOrder()));
  

This sorts by department (A-Z), then name (A-Z), then salary (highest first within same name).

Best Practices

  • Implement Comparable only when a single natural order is meaningful and stable
  • Use Comparator.comparing and method references instead of anonymous classes
  • Chain comparators with thenComparing for multi-field sorting
  • Ensure compareTo is consistent with equals when using TreeSet/TreeMap
  • Use Comparator.nullsFirst() or nullsLast() when null values are possible
  • Prefer Integer.compare(a, b) over a - b to avoid integer overflow bugs

Troubleshooting

Issue Cause Fix
TreeSet drops elements compareTo returns 0 for non-equal objects Make compareTo consistent with equals
IllegalArgumentException: Comparison method violates contract Transitivity broken Review compareTo logic with edge cases
Unexpected sort order Case sensitivity Use String.CASE_INSENSITIVE_ORDER
NullPointerException during sort Null elements in collection Use nullsFirst/nullsLast comparator
Integer overflow in subtraction Using a - b for large values Use Integer.compare(a, b)

Comparable defines how a class sorts itself; Comparator provides flexible, reusable ordering strategies for any type — together they power every sorting operation in the Java collections framework.