Comparable and Comparator in Java

Comparable gives a class one natural order. Comparator supplies any number of orders from outside, without touching the class.

The difference in one line

Comparable is implemented by the class being sorted and defines its single natural order. Comparator is a separate object that defines an order, and a class can be sorted by as many comparators as you like.

Comparable

public record Student(String name, int rollNumber, double marks)
        implements Comparable<Student> {

    @Override
    public int compareTo(Student other) {
        return Integer.compare(rollNumber, other.rollNumber);
    }
}
List<Student> students = new ArrayList<>(List.of(
        new Student("Meera", 12, 88.5),
        new Student("Arun", 4, 76.0),
        new Student("Ravi", 9, 91.25)));

Collections.sort(students);          // uses compareTo
students.sort(null);                 // the same thing
Set<Student> sorted = new TreeSet<>(students);   // also uses compareTo

The contract of compareTo

ReturnMeaning
NegativeThis object comes before the other
ZeroThey have equal ordering
PositiveThis object comes after

The ordering must be consistent: reversing the arguments must reverse the sign, and it must be transitive. It should also be consistent with equals, meaning compareTo returns zero exactly when equals returns true. When it is not, a TreeSet and a HashSet will disagree about what a duplicate is.

Comparator

students.sort(Comparator.comparing(Student::name));
students.sort(Comparator.comparingDouble(Student::marks).reversed());

students.sort(Comparator
        .comparingDouble(Student::marks).reversed()
        .thenComparing(Student::name));            // ties broken by name

The factory methods read almost as English, and they compose. This is the form to use in modern Java; writing the interface out by hand is rarely necessary.

// The explicit form, for comparison
Comparator<Student> byMarks = new Comparator<>() {
    @Override
    public int compare(Student a, Student b) {
        return Double.compare(a.marks(), b.marks());
    }
};

The useful factory methods

MethodPurpose
comparing(fn)Order by an extracted key
comparingInt, comparingLong, comparingDoubleSame, without boxing
thenComparing(fn)Break ties with a second key
reversed()Invert the whole order
naturalOrder(), reverseOrder()Use Comparable
nullsFirst(cmp), nullsLast(cmp)Decide where nulls go
List<String> names = new ArrayList<>(Arrays.asList("Ravi", null, "Anita"));
names.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(names);   // [null, Anita, Ravi]

Never subtract to compare

// Broken: the subtraction overflows
Comparator<Integer> broken = (a, b) -> a - b;
System.out.println(broken.compare(Integer.MAX_VALUE, -1));   // negative, wrong

// Correct
Comparator<Integer> correct = Integer::compare;
Subtracting is a habit carried over from older code and it is a real bug. Always use Integer.compare, Long.compare or Double.compare, which are correct for every input including NaN.

Comparison

AspectComparableComparator
Packagejava.langjava.util
MethodcompareTo(T other)compare(T a, T b)
Where the logic livesInside the classOutside the class
How many ordersOneAny number
Requires changing the classYesNo
Used bysort with no argument, TreeSet, TreeMapsort(cmp), TreeSet(cmp), streams

Implement Comparable when the type has one obvious order, such as a date or a version number. Use a Comparator for everything else, especially for presentation ordering that may change.

With sorted collections and streams

Set<Student> byMarks = new TreeSet<>(
        Comparator.comparingDouble(Student::marks).thenComparing(Student::rollNumber));

Map<String, Integer> sortedByKey = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

List<Student> top = students.stream()
        .sorted(Comparator.comparingDouble(Student::marks).reversed())
        .limit(3)
        .toList();

Optional<Student> best = students.stream()
        .max(Comparator.comparingDouble(Student::marks));

Note the tie breaker in the TreeSet. Without it, two students with equal marks would be treated as the same element and one would be dropped.

Sorting stability

Collections.sort and List.sort are stable: elements that compare equal keep their relative order. That is what makes sorting twice work, first by the secondary key and then by the primary one, although a single chained comparator is clearer.

Common mistakes

  • Subtracting integers inside a comparator.
  • Making compareTo inconsistent with equals and losing elements in a TreeSet.
  • Forgetting a tie breaker in a sorted collection.
  • Sorting an immutable list from List.of, which throws.
  • Returning a boolean converted to 1 or 0 instead of a three way result.
  • Comparing double values with subtraction, which mishandles NaN.

Best practices

  • Build comparators with the factory methods rather than by hand.
  • Use Integer.compare and its siblings.
  • Keep compareTo consistent with equals.
  • Always add a tie breaker for sorted sets and maps.
  • Extract a named comparator constant when the same order is used in several places.

Practice

  1. Sort a list of employees by department, then by salary descending, then by name.
  2. Show a concrete pair of values for which (a, b) -> a - b gives the wrong answer.
  3. Why does a TreeSet drop an element when the comparator returns zero for two unequal objects?
  4. Sort a list containing nulls so that they appear last.
  5. Explain when you would implement Comparable rather than pass a Comparator.

Conclusion

One natural order belongs inside the class as Comparable; every other order belongs outside as a Comparator. Compose comparators with the factory methods, always compare rather than subtract, and remember the tie breaker in sorted collections.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.