Java Comparable vs Comparator — Sorting Objects

3 minute read

Sorting objects in Java requires defining an order. Java provides two interfaces: Comparable for a class’s natural ordering, and Comparator for custom, external ordering.


Comparable — Natural Ordering

A class implements Comparable<T> to define its default sort order:

public class Student implements Comparable<Student> {
    private String name;
    private double gpa;

    @Override
    public int compareTo(Student other) {
        return Double.compare(this.gpa, other.gpa);  // ascending by GPA
    }
}

The Contract

compareTo() returns:

  • Negative — this comes before other
  • Zero — this equals other
  • Positive — this comes after other
List<Student> students = new ArrayList<>(...);
Collections.sort(students);        // uses compareTo()
students.sort(null);                // same — null means natural order

Classes with Natural Ordering

Class Natural Order
String Lexicographic (alphabetical)
Integer, Double, etc. Numeric ascending
LocalDate, LocalDateTime Chronological
Enum Declaration order

Comparator — Custom Ordering

Comparator<T> defines an external, reusable ordering:

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

students.sort(byName);

Lambda Syntax (Java 8+)

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

Method Reference with Comparator.comparing()

students.sort(Comparator.comparing(Student::getName));
students.sort(Comparator.comparingDouble(Student::getGpa));
students.sort(Comparator.comparingInt(Student::getAge));

Comparable vs Comparator

Feature Comparable<T> Comparator<T>
Method compareTo(T) compare(T, T)
Location Inside the class External (separate class/lambda)
Number of orderings One (natural) Unlimited
Modifies original class ✅ ❌
Functional interface ❌ ✅ (lambda-friendly)
Use when There’s one obvious ordering You need multiple orderings

Chaining Comparators

// Sort by GPA descending, then by name ascending
Comparator<Student> comparator = Comparator
    .comparingDouble(Student::getGpa).reversed()
    .thenComparing(Student::getName);

students.sort(comparator);

Available Chaining Methods

Comparator.comparing(keyExtractor)          // sort by key
          .thenComparing(keyExtractor)       // secondary sort
          .thenComparingInt(intExtractor)    // secondary by int
          .thenComparingDouble(dblExtractor) // secondary by double
          .reversed()                        // reverse the entire chain

Handling Nulls

// Nulls first, then sort by name
Comparator<Student> nullSafe = Comparator.nullsFirst(
    Comparator.comparing(Student::getName)
);

// Nulls last
Comparator<Student> nullsLast = Comparator.nullsLast(
    Comparator.comparing(Student::getName)
);

Null-safe Field Comparison

// Student's name might be null
Comparator<Student> byName = Comparator.comparing(
    Student::getName,
    Comparator.nullsLast(Comparator.naturalOrder())
);

Reversing Order

// Reverse natural order
Comparator<String> descending = Comparator.reverseOrder();
names.sort(descending);

// Reverse a specific comparator
Comparator<Student> byGpaDesc = Comparator
    .comparingDouble(Student::getGpa)
    .reversed();

Complete Example

public record Employee(String name, String department, double salary)
        implements Comparable<Employee> {

    @Override
    public int compareTo(Employee other) {
        return Double.compare(this.salary, other.salary);  // natural: by salary
    }
}

List<Employee> employees = List.of(
    new Employee("Alice", "Engineering", 95000),
    new Employee("Bob", "Marketing", 72000),
    new Employee("Charlie", "Engineering", 88000),
    new Employee("Diana", "Marketing", 72000),
    new Employee("Eve", "Engineering", 95000)
);

// Natural order (by salary)
employees.stream().sorted().forEach(System.out::println);

// By department, then salary descending, then name
Comparator<Employee> complex = Comparator
    .comparing(Employee::department)
    .thenComparingDouble(Employee::salary).reversed()
    .thenComparing(Employee::name);

employees.stream().sorted(complex).forEach(System.out::println);

Using with TreeSet and TreeMap

// TreeSet with natural ordering (Comparable)
Set<Student> byGpa = new TreeSet<>();  // uses Student.compareTo()

// TreeSet with custom ordering (Comparator)
Set<Student> byName = new TreeSet<>(Comparator.comparing(Student::getName));

// TreeMap sorted by key
Map<String, Integer> sorted = new TreeMap<>();  // natural String order
Map<String, Integer> reversed = new TreeMap<>(Comparator.reverseOrder());

Warning: TreeSet/TreeMap uses compareTo() or compare() for equality. If compare(a, b) == 0, the tree considers a and b equal — even if a.equals(b) is false.


Writing a Good compareTo()

// ✅ CORRECT — using built-in compare methods
@Override
public int compareTo(Student other) {
    int result = this.name.compareTo(other.name);
    if (result != 0) return result;
    return Double.compare(this.gpa, other.gpa);
}

// ❌ BAD — integer overflow risk
@Override
public int compareTo(Student other) {
    return this.age - other.age;  // overflow if values are extreme!
}

// ✅ CORRECT — use Integer.compare for int fields
@Override
public int compareTo(Student other) {
    return Integer.compare(this.age, other.age);
}

Consistency with equals()

It’s strongly recommended that compareTo() is consistent with equals():

a.compareTo(b) == 0  should mean  a.equals(b) == true

If they’re inconsistent, sorted collections (TreeSet, TreeMap) may behave unexpectedly — e.g., a TreeSet might consider two non-equal objects as duplicates.


Key Takeaways

  1. Comparable = one natural ordering, defined inside the class (compareTo).
  2. Comparator = custom/multiple orderings, defined externally (compare).
  3. Use Comparator.comparing() + thenComparing() for clean, readable chains.
  4. Use nullsFirst() / nullsLast() for null-safe sorting.
  5. Use Integer.compare(), Double.compare() — never subtract for comparison.
  6. Keep compareTo() consistent with equals() for predictable behaviour in sorted collections.

What’s Next?

Next: Introduction to Generics — type safety, generic classes, methods, and type erasure.

Updated: