Java Comparator Interface

In the last lesson you learned about the Java Comparable interface, which gives a class one natural order. But real programs often need to sort the same objects in different ways, which a single compareTo cannot do. For multiple, flexible orderings, Java has the Comparator.

πŸ€” Why Comparable is not enough

Imagine a school app. One page shows a leaderboard by marks. Another shows a roster by name. A third shows students youngest to oldest. That is three orders for the same objects.

// Comparable gives ONE order (say, by age). But what if you also need:
// - sort by name
// - sort by marks
// - sort by age descending
// You cannot put all of these in one compareTo.

Where Comparable runs out:

  • compareTo lives inside the class, so it offers only one order.
  • Some classes you cannot edit, like String or a library type, so you cannot add compareTo at all.
  • A Comparator lives outside the class, so you can make as many orders as you like.

🧩 What is a Comparator?

A Comparator is a separate object whose only job is to compare two things and say which comes first. Think of it like a referee you hand to the sorting method. The list cannot rank itself, so the referee decides.

The key ideas:

  • It is an object you create, separate from the class being sorted.
  • Its single method is compare(a, b), which takes two arguments.
  • You can build many comparators for the same type, one per ordering.

compare(a, b) follows the same sign contract as compareTo:

  • Return a negative number when a comes before b.
  • Return zero when a and b are equal in this ordering.
  • Return a positive number when a comes after b.
  • Only the sign matters, not the size.

Here is the long-hand way first, so you can see all the parts. This comparator orders students by name.

import java.util.Comparator;
// a comparator that orders Students by name
Comparator<Student> byName = new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return a.name.compareTo(b.name); // reuse String's own ordering
}
};

What to notice here:

  • byName is standalone. It is not part of the Student class.
  • Inside compare, we lean on String’s own compareTo and hand the result back.
  • You could make another comparator for age or marks and pick one at the call site.

πŸ”§ Passing a Comparator to a sort

A comparator does nothing on its own. You hand it to something that sorts.

  • Collections.sort(list, comparator) sorts a list in place using your rule.
  • list.sort(comparator) does the same, called directly on the list.
  • new TreeSet<>(comparator) keeps elements ordered by your rule as you add them.
  • new TreeMap<>(comparator) orders its keys by your rule.

The two list calls do the same work, but list.sort(...) is the newer, shorter style. This example sorts the same list with the byName comparator.

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
ArrayList<Student> students = new ArrayList<>();
// ... students added here ...
// both lines below do the same thing
Collections.sort(students, byName); // older style
students.sort(byName); // newer, shorter style

One catch with TreeSet and TreeMap. They use the comparator to decide ordering and to spot duplicates. If your comparator returns zero for two students, a TreeSet keeps only one. Keep that in mind for tree-based collections.

⚑ The clean way: lambdas and comparing

A full anonymous class like byName is wordy. Because Comparator is a functional interface (one abstract method), you can write it as a lambda instead, a short inline method.

// the whole anonymous class above becomes one lambda
Comparator<Student> byMarks = (a, b) -> a.getMarks() - b.getMarks();

That reads as β€œto compare a and b, subtract their marks.” It works, but the subtraction trick has a hidden bug we cover in Common Mistakes. There is a safer, cleaner option: factory methods that build comparators when you point them at a field.

  • Comparator.comparing works for any field.
  • comparingInt and comparingDouble are for number fields.

Here we sort the same list by age, then by name.

import java.util.ArrayList;
import java.util.Comparator;
class Student {
String name;
int age;
Student(String name, int age) { this.name = name; this.age = age; }
public String toString() { return name + "(" + age + ")"; }
}
public class Main {
public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>();
students.add(new Student("Alex", 22));
students.add(new Student("Riya", 20));
students.add(new Student("Arjun", 25));
students.sort(Comparator.comparingInt(s -> s.age)); // βœ… by age
System.out.println("By age: " + students);
students.sort(Comparator.comparing(s -> s.name)); // βœ… by name
System.out.println("By name: " + students);
}
}

Walking through the two sort lines:

  • Comparator.comparingInt(s -> s.age) pulls age out of each student and sorts by it, with no class and no subtraction.
  • Comparator.comparing(s -> s.name) does the same for the name field.
  • Same list, sorted two ways, in two clean lines. This is the modern, recommended style.

A note on the helpers:

  • comparing(s -> s.field) works for any field, including objects like String.
  • comparingInt(s -> s.field) is for int fields and avoids boxing overhead.
  • comparingDouble(s -> s.field) is the same idea for double fields.

Output

By age: [Riya(20), Alex(22), Arjun(25)]
By name: [Alex(22), Arjun(25), Riya(20)]

πŸ”„ Reversing and chaining

Comparators snap together like building blocks. Two methods do most of the work.

  • reversed() flips an order from ascending to descending.
  • thenComparing adds a tie-breaker, used only when the first comparison comes out equal.

This example builds two combined comparators.

import java.util.Comparator;
// age, highest first
Comparator<Student> byAgeDesc = Comparator.comparingInt((Student s) -> s.age).reversed();
// by age, and when ages are equal, break the tie by name
Comparator<Student> byAgeThenName =
Comparator.comparingInt((Student s) -> s.age)
.thenComparing(s -> s.name);

Reading these:

  • reversed() turns the ascending age order upside down, so oldest comes first.
  • thenComparing(s -> s.name) breaks ties: same age, sort those by name.
  • You can keep chaining thenComparing for more levels. Cleaner than nested if-statements.
  • (Student s) is written out because Java sometimes cannot guess a lambda’s type at the start of a chain. The explicit type tells the rest of the chain what s is.

πŸ› οΈ A full worked example: marks, then name, then descending

Let’s build a leaderboard sorted by marks. When marks tie, order by name so the result is predictable. Then make a second view that flips it for lowest scorers first.

Here is the complete program.

import java.util.ArrayList;
import java.util.Comparator;
class Student {
String name;
int marks;
Student(String name, int marks) { this.name = name; this.marks = marks; }
public int getMarks() { return marks; }
public String getName() { return name; }
public String toString() { return name + "=" + marks; }
}
public class Main {
public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>();
students.add(new Student("Alex", 90));
students.add(new Student("Riya", 90));
students.add(new Student("Arjun", 75));
students.add(new Student("Meera", 90));
// marks ascending, tie broken by name
Comparator<Student> byMarksThenName =
Comparator.comparingInt(Student::getMarks)
.thenComparing(Student::getName);
students.sort(byMarksThenName);
System.out.println("Marks then name: " + students);
// same rule, but flipped to descending
students.sort(byMarksThenName.reversed());
System.out.println("Reversed: " + students);
}
}

What to point out here:

  • Student::getMarks is a method reference, shorthand for a lambda that calls one method. It reads as β€œcompare by getMarks, then by getName.”
  • Three students share 90 marks, so the name tie-breaker decides their order, giving a stable result.
  • The reversed view flips that same combined rule.

Output

Marks then name: [Arjun=75, Alex=90, Meera=90, Riya=90]
Reversed: [Riya=90, Meera=90, Alex=90, Arjun=75]

The reversed list flips everything, including the tie-break, so within the 90s the names now run Riya, Meera, Alex. reversed() reverses the complete combined rule, not just the marks part.

πŸ•³οΈ Handling nulls safely

Sorting throws a NullPointerException the moment your comparator reads a field from a null element. Java gives you wrappers for this. nullsFirst puts null values at the start, nullsLast at the end, then applies your real comparator to the rest.

This comparator sorts by name but sends null students to the back.

import java.util.Comparator;
// null students go last; the rest are sorted by name
Comparator<Student> safeByName =
Comparator.nullsLast(Comparator.comparing(s -> s.name));

The point, in short:

  • Use nullsFirst(...) to push null elements to the front.
  • Use nullsLast(...) to push them to the back.
  • Wrap your normal comparator inside one of these whenever nulls are possible.

βš–οΈ Comparable vs Comparator

Both define how to compare objects. They differ in where the rule lives and how many you can have.

Feature Comparable Comparator
Where it lives Inside the class Outside, as a separate object
Method compareTo(other) compare(a, b)
How many orders One (the natural order) As many as you want
Classes you do not own Cannot add it Works fine, defined outside
Best for The single default order Multiple or custom orderings

Rule of thumb:

  • Use Comparable for the one natural order that belongs to the class, the order most code expects.
  • Use Comparator for several orderings, for a field the class never ordered by, or for a class you cannot edit.
  • They are not rivals. A class can have a natural order and still be sorted by comparators.

⚠️ Common Mistakes

A few Comparator slip-ups that bite people.

The subtraction trick can overflow. (a, b) -> a.getValue() - b.getValue() looks neat, but int subtraction wraps around for far-apart numbers, like a large positive minus a large negative. The sign flips and the sort comes out wrong with no error.

// ❌ subtraction can overflow for large/small ints and give the wrong order
Comparator<Student> bad = (a, b) -> a.getMarks() - b.getMarks();
// βœ… comparingInt does the comparison safely
Comparator<Student> good = Comparator.comparingInt(Student::getMarks);

Sorting changes order, not the objects. A comparator only rearranges elements. It never edits their fields. After sorting, every student keeps the same name, age, and marks. Only positions changed.

// ❌ wrong expectation: sorting does NOT change any student's fields
// students.sort(byMarks); // Alex still has the same marks afterward
// βœ… correct: it only reorders the elements, the data stays the same

Mixing up the two interfaces. Comparable uses compareTo(other) and lives inside the class. Comparator uses compare(a, b) with two arguments and lives outside. Wrong method name or wrong argument count is a common compile error.

// ❌ a Comparator does NOT have compareTo
// public int compareTo(Student a, Student b) { ... }
// βœ… a Comparator implements compare with TWO arguments
public int compare(Student a, Student b) { return a.age - b.age; }

βœ… Best Practices

Habits that keep comparator code clean and correct.

  • Prefer Comparator.comparing and friends. Reach for comparing, comparingInt, and comparingDouble before writing a manual compare. They are shorter, clearer, and avoid the overflow bug.
  • Use method references where you can. Student::getMarks reads better than s -> s.getMarks().
  • Chain thenComparing for predictable ties. A tie-breaker makes the result stable and repeatable.
  • Use reversed() for descending. Build the ascending comparator, then flip it, rather than negating numbers by hand.
  • Wrap with nullsFirst or nullsLast whenever an element or field might be null.
  • Keep Comparable for the one natural order. Use Comparator for everything else.

🧩 What You’ve Learned

Nicely done. Let’s recap Comparator.

  • βœ… A Comparator is a separate object that defines an ordering, so you can have many orders, even for classes you do not own.
  • βœ… It lives outside the class and uses compare(a, b) with two arguments, following the negative/zero/positive rule.
  • βœ… You pass it to Collections.sort, list.sort, or a TreeSet/TreeMap.
  • βœ… Build them cleanly with Comparator.comparing, comparingInt, and comparingDouble plus lambdas or method references.
  • βœ… Combine them with reversed() for descending, thenComparing for tie-breakers, and nullsFirst/nullsLast for safety.
  • βœ… Use Comparable for the one natural order, Comparator for multiple or custom orderings.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What is the main advantage of a Comparator over Comparable?

    Why: Comparator is defined outside the class, so you can create several different sort orders and even sort classes you cannot edit.

  2. 2

    Which method does a Comparator use?

    Why: A Comparator implements compare(a, b), taking two objects to compare.

  3. 3

    Why prefer Comparator.comparingInt over a -> a - b subtraction?

    Why: Subtracting ints can overflow for far-apart values, silently flipping the sign; comparingInt compares safely.

  4. 4

    How do you sort in descending order with a comparator?

    Why: reversed() flips an existing comparator from ascending to descending.

πŸš€ What’s Next?

Next we move into generics, which let you write classes and methods that work with any type while keeping your code type-safe. Let’s start with the basics.

Java Introduction to Generics

Share & Connect