Skip to content
elephantoo

Comparable vs Comparator

Lesson 28 of 43 14 min read

Natural ordering with compareTo, custom orderings with Comparator.comparing, thenComparing and reversed.


Sorting a list of numbers or strings just works: Collections.sort(names). But how should Java sort a list of Employees? By name? Salary? Joining date? Java offers two tools for defining order:

  • Comparable: the class defines its own natural order (one, built in).
  • Comparator: a separate object defining any order you like (as many as you need).

The comparison contract#

Both use the same convention. Comparing a with b returns an int:

ResultMeaning
negativea comes before b
zeroa and b are equal in ordering
positivea comes after b

Comparable: natural ordering#

Implement Comparable<T> and its single method compareTo:

NaturalOrder.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TreeSet;

public class NaturalOrder {
    static class Version implements Comparable<Version> {
        final int major, minor, patch;

        Version(int major, int minor, int patch) {
            this.major = major; this.minor = minor; this.patch = patch;
        }

        @Override
        public int compareTo(Version other) {
            int c = Integer.compare(major, other.major);
            if (c != 0) return c;
            c = Integer.compare(minor, other.minor);
            if (c != 0) return c;
            return Integer.compare(patch, other.patch);
        }

        @Override
        public String toString() { return major + "." + minor + "." + patch; }
    }

    public static void main(String[] args) {
        List<Version> versions = new ArrayList<>(List.of(
            new Version(2, 0, 1), new Version(1, 10, 0), new Version(1, 2, 3), new Version(2, 0, 0)));

        Collections.sort(versions);                  // uses compareTo
        System.out.println(versions);
        System.out.println(Collections.max(versions));
        System.out.println(new TreeSet<>(versions).first());
        System.out.println(new Version(1, 2, 3).compareTo(new Version(1, 10, 0)) < 0);
    }
}
Output
[1.2.3, 1.10.0, 2.0.0, 2.0.1]
2.0.1
1.2.3
true

Many JDK classes already implement Comparable: String (alphabetical), Integer and other wrappers (numeric), LocalDate (chronological), and enums (declaration order).

Never implement compareTo with subtraction (return a - b;). It overflows for large values and silently gives the wrong sign. Use Integer.compare, Double.compare and friends.

Ideally compareTo is consistent with equals: a.compareTo(b) == 0 exactly when a.equals(b). TreeSet and TreeMap use compareTo to detect duplicates, so inconsistency causes surprising behaviour.

Comparator: orders defined outside the class#

A Comparator<T> is a separate object with a compare(a, b) method. Use it when:

  • the class has no natural order, or you can't modify it;
  • you need several different orders (by price, by rating, by name);
  • you want an order different from the natural one (reverse, case-insensitive).

Comparator is a functional interface, so lambdas work:

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

public class Comparators {
    record Product(String name, double price, double rating) { }

    public static void main(String[] args) {
        List<Product> products = new ArrayList<>(List.of(
            new Product("Mouse", 499, 4.3),
            new Product("Keyboard", 1299, 4.6),
            new Product("Cable", 199, 4.3),
            new Product("Monitor", 8999, 4.8)));

        products.sort((a, b) -> Double.compare(a.price(), b.price()));   // lambda
        System.out.println(names(products));

        products.sort(Comparator.comparing(Product::name));               // key extractor
        System.out.println(names(products));

        products.sort(Comparator.comparingDouble(Product::rating).reversed());
        System.out.println(names(products));

        products.sort(Comparator.comparingDouble(Product::rating).reversed()
                .thenComparingDouble(Product::price));                    // tie-breaker
        System.out.println(names(products));
    }

    static List<String> names(List<Product> ps) {
        List<String> out = new ArrayList<>();
        for (Product p : ps) out.add(p.name());
        return out;
    }
}
Output
[Cable, Mouse, Keyboard, Monitor]
[Cable, Keyboard, Monitor, Mouse]
[Monitor, Keyboard, Cable, Mouse]
[Monitor, Keyboard, Cable, Mouse]

Mouse and Cable both have rating 4.3. On the third line they appear as Cable, Mouse only because sort is stable: tied elements keep their previous order, which happened to be alphabetical from the earlier sort. The fourth line gets the same result on purpose: the explicit tie-breaker (price ascending) puts the cheaper Cable first no matter what order the list was in before. Don't rely on leftovers from earlier sorts; state your tie-breakers.

The Comparator toolkit#

MethodPurpose
Comparator.comparing(keyFn)order by a key that is Comparable
comparingInt/Long/Double(keyFn)same, for primitive keys (no boxing)
comparing(keyFn, keyComparator)order by a key with a custom key order
.thenComparing(...)tie-breaker when the previous comparison is equal
.reversed()reverse everything built so far
Comparator.naturalOrder() / reverseOrder()natural order and its reverse
Comparator.nullsFirst(c) / nullsLast(c)handle null elements or keys safely
String.CASE_INSENSITIVE_ORDERcase-insensitive string order

Watch where reversed() goes#

reversed() applies to the entire chain to its left:

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

public class ReversedTrap {
    record Emp(String dept, String name, int salary) { }

    public static void main(String[] args) {
        List<Emp> staff = new ArrayList<>(List.of(
            new Emp("Eng", "Ira", 90), new Emp("Ops", "Om", 70),
            new Emp("Eng", "Ali", 120), new Emp("Ops", "Zia", 85)));

        // dept A-Z, then salary high-to-low
        staff.sort(Comparator.comparing(Emp::dept)
                .thenComparing(Emp::salary, Comparator.reverseOrder()));
        staff.forEach(e -> System.out.print(e.name() + " "));
        System.out.println();

        // WRONG for that goal: reverses dept order as well
        staff.sort(Comparator.comparing(Emp::dept)
                .thenComparingInt(Emp::salary).reversed());
        staff.forEach(e -> System.out.print(e.name() + " "));
        System.out.println();
    }
}
Output
Ali Ira Zia Om
Zia Om Ali Ira

Handling nulls and strings#

NullsAndCase.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class NullsAndCase {
    public static void main(String[] args) {
        List<String> cities = new ArrayList<>(Arrays.asList("pune", null, "Delhi", "agra", null, "Bhopal"));

        cities.sort(Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER));
        System.out.println(cities);

        cities.sort(Comparator.nullsFirst(Comparator.<String>naturalOrder().reversed()));
        System.out.println(cities);

        List<String> words = new ArrayList<>(List.of("bb", "a", "ccc", "dd"));
        words.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.reverseOrder()));
        System.out.println(words);
    }
}
Output
[agra, Bhopal, Delhi, pune, null, null]
[null, null, pune, agra, Delhi, Bhopal]
[a, dd, bb, ccc]

Without nullsFirst/nullsLast, sorting a list containing null throws NullPointerException.

Where comparators are used#

  • list.sort(cmp) and Collections.sort(list, cmp).
  • Arrays.sort(array, cmp) for object arrays.
  • new TreeSet<>(cmp), new TreeMap<>(cmp), new PriorityQueue<>(cmp).
  • Collections.max(coll, cmp) and min.
  • Streams: stream.sorted(cmp), max(cmp), min(cmp) (Streams lesson).
TopN.java
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;

public class TopN {
    record Score(String player, int points) { }

    public static void main(String[] args) {
        List<Score> scores = List.of(new Score("A", 40), new Score("B", 95),
                new Score("C", 70), new Score("D", 88), new Score("E", 12));

        PriorityQueue<Score> best = new PriorityQueue<>(
                Comparator.comparingInt(Score::points).reversed());   // max-heap
        best.addAll(scores);
        for (int i = 0; i < 3; i++) {
            Score s = best.poll();
            System.out.println((i + 1) + ". " + s.player() + " " + s.points());
        }
    }
}
Output
1. B 95
2. D 88
3. C 70

Comparable or Comparator?#

ComparableComparator
Packagejava.langjava.util
MethodcompareTo(T other)compare(T a, T b)
Whereinside the classoutside: a lambda or a separate class
How many ordersone (natural)unlimited
Modify the class?yesno
ExampleString, Integer, LocalDateComparator.comparing(Person::age)

Give a class a natural order only if there is one obvious ordering (versions, dates, money amounts). For everything else, use comparators at the point of sorting.

Common mistakes#

  • Using subtraction in compareTo/compare.
  • Putting reversed() at the end of a chain when you only meant to reverse one key.
  • Sorting lists containing null without nullsFirst/nullsLast.
  • A compareTo inconsistent with equals, causing a TreeSet to silently drop "duplicates".
  • Writing raw Comparator without its type parameter, which loses type safety.

What's next#

Comparators define order. Hash-based collections need to know about equality. The next lesson explains equals() and hashCode(): the contract, how to implement them correctly, and what breaks if you don't.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What should a.compareTo(b) return when a should come before b?

  2. 2.Why is return this.age - other.age; a risky compareTo implementation?

  3. 3.Which comparator sorts employees by department, then by salary from highest to lowest?

Finished reading?

Mark this lesson complete to track your progress.