Course topics

By WebNest Studio Engineering Team

Java - Core Tutorial

Comparable vs Comparator

Java offers two distinct ways to define an ordering for objects: implementing the Comparable interface inside the class itself, or writing a separate Comparator. They solve related but different problems, and understanding when to use each is essential for sorting collections and building sorted data structures like TreeSet and TreeMap.

Why it matters

Sorting objects is an everyday task: employees by salary, orders by date, products by price and then by name. Java can only sort objects if it is told how to compare them, and there are two ways to do that. Knowing which to use, and how to chain comparators for several fields, covers nearly every sorting requirement you will meet.

Comparable — One Natural Ordering, Defined Inside the Class

A class implements Comparable<T> by providing a single method: compareTo(T other), which returns a negative number, zero, or a positive number depending on whether "this" object is less than, equal to, or greater than "other". This defines the class's natural ordering — the default order used by Collections.sort(), Arrays.sort(), TreeSet, and TreeMap when no other ordering is specified. A class can only have one natural ordering, because compareTo() is a single fixed method.

Comparator — Multiple External Orderings

A Comparator<T> is a separate object defining a compare(T a, T b) method, written independently of the class being compared. This lets you define as many different orderings as you need — by name, by age, by price descending — without modifying the target class at all, and is especially useful for classes you don't own (like library classes) or classes with no obviously "natural" order.

Modern Comparator Construction

Since Java 8, Comparator is a functional interface, so it is commonly built with lambdas or the static helper methods Comparator.comparing(), .thenComparing(), and .reversed(), which read almost like a sentence describing the sort order.

  • Comparable: compareTo(T o), one ordering, defined inside the class, used automatically by sort().
  • Comparator: compare(T a, T b), unlimited orderings, defined externally, passed explicitly to sort().

Example

Natural ordering via Comparable, plus two external orderings via Comparator

Java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

class Employee implements Comparable<Employee> {
    String name;
    int age;

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

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

    @Override
    public String toString() {
        return name + "(" + age + ")";
    }
}

public class ComparableComparatorDemo {
    public static void main(String[] args) {
        List<Employee> team = new ArrayList<>(List.of(
                new Employee("Ravi", 34),
                new Employee("Asha", 28),
                new Employee("Meera", 41)
        ));

        java.util.Collections.sort(team); // uses Comparable (natural order: by age)
        System.out.println("By age (natural): " + team);

        team.sort(Comparator.comparing(e -> e.name)); // Comparator: by name
        System.out.println("By name (Comparator): " + team);

        team.sort(Comparator.comparingInt((Employee e) -> e.age).reversed());
        System.out.println("By age descending (Comparator): " + team);
    }
}
Output
By age (natural): [Asha(28), Ravi(34), Meera(41)]
By name (Comparator): [Asha(28), Meera(41), Ravi(34)]
By age descending (Comparator): [Meera(41), Ravi(34), Asha(28)]

Common Mistakes

  • Trying to implement Comparable twice with different logic — a class can define only one compareTo() method, so it has exactly one natural ordering.
  • Returning inconsistent results from compareTo() or compare() (not respecting antisymmetry/transitivity), which corrupts TreeSet/TreeMap ordering silently.
  • Using compareTo() subtraction tricks like "a.getAge() - b.getAge()" which can silently overflow for large or negative int values — Integer.compare() is safer.
  • Forgetting that TreeSet/TreeMap use compareTo()/compare() (not equals()) to determine element uniqueness, which can cause unexpected "duplicate" rejection.

Exercise

Sort the employees by salary, highest first. Employees with the same salary should be ordered by name. Then print the names.

Expected output: Ravi, Asha, Zoya (one per line)

Java · starter code
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

class Employee {
    private final String name;
    private final int salary;

    Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    String getName() { return name; }
    int getSalary() { return salary; }
}

public class SortEmployees {
    public static void main(String[] args) {
        List<Employee> staff = new ArrayList<>(List.of(
                new Employee("Zoya", 50000),
                new Employee("Ravi", 70000),
                new Employee("Asha", 50000)));

        // TODO: sort by salary descending, then by name

        for (Employee e : staff) {
            System.out.println(e.getName());
        }
    }
}
Show hints
  • Start with Comparator.comparingInt(Employee::getSalary) and call reversed() on it.
  • Add the second rule with thenComparing(Employee::getName), after reversed() so that names stay in ascending order.
Show solution
Java · solution
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

class Employee {
    private final String name;
    private final int salary;

    Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    String getName() { return name; }
    int getSalary() { return salary; }
}

public class SortEmployees {
    public static void main(String[] args) {
        List<Employee> staff = new ArrayList<>(List.of(
                new Employee("Zoya", 50000),
                new Employee("Ravi", 70000),
                new Employee("Asha", 50000)));

        staff.sort(Comparator.comparingInt(Employee::getSalary).reversed()
                .thenComparing(Employee::getName));

        for (Employee e : staff) {
            System.out.println(e.getName()); // Ravi, Asha, Zoya
        }
    }
}

Quick quiz

1. Which method does a class implement to define its natural ordering?

  • A. compare(a, b) from Comparator
  • B. compareTo(other) from Comparable
  • C. equals(other)
  • D. sort()
Show answer

B. Comparable has one method, compareTo, written inside the class being compared.

2. What should a.compareTo(b) return when a comes before b?

  • A. A positive number
  • B. Zero
  • C. A negative number
  • D. true
Show answer

C. Negative means a is less than b, zero means they are equal in order, positive means a is greater.

3. You need to sort the same list of products by price on one screen and by name on another. What should you use?

  • A. Two Comparable implementations in the Product class
  • B. It is not possible
  • C. A TreeSet
  • D. One Comparator for each ordering
Show answer

D. A class can have only one compareTo, but any number of comparators can be written for it.

Interview questions

What is the difference between Comparable and Comparator?

Show answer

Comparable is implemented by the class itself through compareTo() and defines its single natural ordering; String and Integer are examples. Comparator is a separate object with a compare(a, b) method, so you can define many orderings for one class and sort classes whose source you cannot change. Sorting methods and sorted collections use the natural ordering unless a comparator is passed.

Why is "return this.age - other.age" a risky way to write compareTo?

Show answer

Subtracting two int values can overflow when they are far apart, for example a large positive number minus a large negative number, and the result then has the wrong sign. Integer.compare(this.age, other.age) always returns the correct sign and should be used instead.

What does it mean for compareTo to be consistent with equals?

Show answer

It means a.compareTo(b) == 0 exactly when a.equals(b) is true. Sorted collections such as TreeSet and TreeMap use the comparison, not equals(), to decide whether two elements are the same. If the two disagree, a TreeSet can drop an element that a HashSet would keep.

Key Points to Remember

  • Comparable defines a single natural ordering inside the class via compareTo(); used automatically by sort() and by TreeSet/TreeMap.
  • Comparator defines an ordering externally via compare(), and any number of them can exist for the same class.
  • Comparator.comparing().thenComparing().reversed() makes multi-field, readable sort logic easy to build with lambdas.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering