Java Comparator Interface
Table of Contents + β
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:
compareTolives inside the class, so it offers only one order.- Some classes you cannot edit, like
Stringor a library type, so you cannot addcompareToat all. - A
Comparatorlives 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
acomes beforeb. - Return zero when
aandbare equal in this ordering. - Return a positive number when
acomes afterb. - 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 nameComparator<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:
byNameis standalone. It is not part of theStudentclass.- Inside
compare, we lean onStringβs owncompareToand 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 thingCollections.sort(students, byName); // older stylestudents.sort(byName); // newer, shorter styleOne 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 lambdaComparator<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.comparingworks for any field.comparingIntandcomparingDoubleare 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)pullsageout 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 likeString.comparingInt(s -> s.field)is forintfields and avoids boxing overhead.comparingDouble(s -> s.field)is the same idea fordoublefields.
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.thenComparingadds a tie-breaker, used only when the first comparison comes out equal.
This example builds two combined comparators.
import java.util.Comparator;
// age, highest firstComparator<Student> byAgeDesc = Comparator.comparingInt((Student s) -> s.age).reversed();
// by age, and when ages are equal, break the tie by nameComparator<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
thenComparingfor 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 whatsis.
π οΈ 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::getMarksis 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 nameComparator<Student> safeByName = Comparator.nullsLast(Comparator.comparing(s -> s.name));The point, in short:
- Use
nullsFirst(...)to pushnullelements 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
Comparatorfor 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 orderComparator<Student> bad = (a, b) -> a.getMarks() - b.getMarks();
// β
comparingInt does the comparison safelyComparator<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 sameMixing 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 argumentspublic int compare(Student a, Student b) { return a.age - b.age; }β Best Practices
Habits that keep comparator code clean and correct.
- Prefer
Comparator.comparingand friends. Reach forcomparing,comparingInt, andcomparingDoublebefore writing a manual compare. They are shorter, clearer, and avoid the overflow bug. - Use method references where you can.
Student::getMarksreads better thans -> s.getMarks(). - Chain
thenComparingfor 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
nullsFirstornullsLastwhenever an element or field might benull. - 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 aTreeSet/TreeMap. - β
Build them cleanly with
Comparator.comparing,comparingInt, andcomparingDoubleplus lambdas or method references. - β
Combine them with
reversed()for descending,thenComparingfor tie-breakers, andnullsFirst/nullsLastfor 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
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
Which method does a Comparator use?
Why: A Comparator implements compare(a, b), taking two objects to compare.
- 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
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.