One class, three rank lists
The exam cell needs three printouts of the same students: by roll number for the attendance register, by marks (highest first) for the merit list, and by name for the certificate desk. Kabir calls Collections.sort(students) and gets a compile error.
Sir, Java sorts numbers and names fine. Why can't it sort students?
Because it doesn't know what “smaller” means for a student. Roll number? Marks? Name? You choose. If one order is the obvious, everyday one, like roll number, put it inside the class by implementing Comparable.
And the merit list and certificates?
Those are extra orders, so write them as Comparators outside the class and pass them to sort. One class, as many orders as the exam cell wants, and modern Java builds them in one line.
What are Comparable and Comparator?
Comparable<T> (java.lang) is implemented by a class to define its natural ordering through the single method int compareTo(T other). Comparator<T> (java.util) is a separate object that defines an ordering through int compare(T a, T b), allowing multiple orderings and ordering of classes you can't modify. Both return a negative integer if the first argument comes before the second, zero if they are equal in the ordering, and a positive integer if it comes after. Sorting methods (Collections.sort, List.sort, Arrays.sort) and sorted collections (TreeSet, TreeMap, PriorityQueue) use the natural ordering unless a Comparator is supplied.
Comparable: “this is how my objects are normally ordered”, written in the class. Comparator: “sort them this way for now”, written anywhere.
Words you'll meet in this lesson
Student::marks means “call marks() on each student” (Lesson 14.1).Comparable vs Comparator
Comparable | Comparator | |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(T other) | compare(T a, T b) |
| Where | Inside the class being sorted | A separate object (class, lambda, method reference) |
| How many orders | One natural order | As many as you like |
| Use with | Collections.sort(list), new TreeSet<>() | list.sort(cmp), new TreeSet<>(cmp) |
| Classes you can't edit | Not possible | Works fine |
Exam tip: only the sign of the result matters (negative / zero / positive), not the size. And compareTo should be consistent with equals: a.compareTo(b) == 0 exactly when a.equals(b); otherwise TreeSet/TreeMap behave strangely (Lesson 11.5).
From one natural order to many custom ones
Comparable
Implement compareTo in the class. Sorting needs no arguments.
Comparator class / lambda
(a, b) -> Integer.compare(a.marks, b.marks)
Comparator builders
comparing, thenComparing, reversed, nullsFirst.
Comparable: the natural order
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Student implements Comparable<Student> {
final int roll;
final String name;
final int marks;
Student(int roll, String name, int marks) {
this.roll = roll;
this.name = name;
this.marks = marks;
}
@Override
public int compareTo(Student other) {
return Integer.compare(this.roll, other.roll); // by roll number
}
@Override
public String toString() { return roll + " " + name + " " + marks; }
}
public class Main {
public static void main(String[] args) {
List<Student> list = new ArrayList<>(List.of(
new Student(21, "Om", 92), new Student(7, "Riya", 92), new Student(12, "Kabir", 78)));
Collections.sort(list); // uses compareTo
System.out.println(list);
}
}Comparators: any order you need
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class Student {
final int roll; final String name; final int marks;
Student(int roll, String name, int marks) { this.roll = roll; this.name = name; this.marks = marks; }
int roll() { return roll; }
String name() { return name; }
int marks() { return marks; }
@Override public String toString() { return roll + " " + name + " " + marks; }
}
public class Main {
public static void main(String[] args) {
List<Student> list = new ArrayList<>(List.of(
new Student(21, "Om", 92), new Student(7, "Riya", 92), new Student(12, "Kabir", 78)));
// 1. A Comparator written as a lambda (Lesson 14.1)
list.sort((a, b) -> a.name.compareTo(b.name));
System.out.println("By name: " + list);
// 2. Built with Comparator.comparing: marks high→low, ties by roll
Comparator<Student> merit = Comparator.comparingInt(Student::marks).reversed()
.thenComparingInt(Student::roll);
list.sort(merit);
System.out.println("Merit: " + list);
}
}Strings: case matters
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("riya", "Zoya", "Kabir"));
Collections.sort(names); // natural: by char codes
System.out.println(names);
names.sort(String.CASE_INSENSITIVE_ORDER); // a ready-made Comparator
System.out.println(names);
names.sort(Comparator.reverseOrder());
System.out.println(names);
}
}The subtraction trap
public class Main {
public static void main(String[] args) {
int a = -2_000_000_000, b = 2_000_000_000;
System.out.println(a - b); // overflows: looks positive!
System.out.println(Integer.compare(a, b)); // correct: negative
}
}Don't write return this.x - other.x; in compareTo. For large or negative values the subtraction overflows and the sign flips, so the sort silently goes wrong. Use Integer.compare, Long.compare, Double.compare or Comparator.comparingInt.
Rule of thumb: give a class a natural order only if there's one obvious, stable order (IDs, dates, version numbers). Everything else is a Comparator, ideally a named constant like Student.BY_MERIT built with comparing(...).thenComparing(...).
What sort does with your comparator
You hand over the rule
list.sort(merit);List.sortcopies the elements into an array and callsArrays.sort(array, merit).The algorithm picks pairs
Java sorts objects with TimSort, a merge sort tuned for real data (it finds already-sorted runs). For tiny arrays it uses binary insertion sort. It decides which two elements to compare next.Your method answers: before, same or after
For each pair it callsmerit.compare(a, b)(ora.compareTo(b)). Your code never moves elements; it only answers with a sign. The algorithm moves them.About n log n questions
Sorting n elements needs roughly n·log₂n comparisons: about 10,000 for 1,000 students. That's why a comparator should be cheap and must be consistent (if a < b and b < c then a < c), or TimSort may throw Comparison method violates its general contract!.Stable result, written back
The sorted array is copied back into the list. Because the sort is stable, students with equal keys stay in their previous relative order, which lets you sort by a secondary key first and the primary key second if you want.
sort asks, your comparator answers: both views
Left: the list being sorted for the merit list. Right: each call to compare, its result, and what the sort does with it.
- The list starts in arrival order. We ask for the merit order.
- Nothing visible happens yet.
- Still nothing visible.
- Riya and Om change places.
- Kabir stays where he is.
- The merit list: Riya (92, roll 7), Om (92, roll 21), Kabir (78).
list.sort(merit)copies the list into an array and starts TimSort. For tiny arrays it looks for an ordered run and then inserts the rest.- It asks
merit.compare(Riya, Om). Marks are equal (92), so the tie-breaker compares rolls 7 and 21: −1. In merit order Riya comes first, so the first two elements form a descending run. - Does the run continue?
compare(Kabir, Riya): 78 vs 92 withreversed()gives +1, so no. The run is [Om, Riya]. - A descending run is reversed in place: [Riya, Om, Kabir].
- Kabir is inserted into the sorted part using binary search:
compare(Kabir, Om)= +1, so he goes after Om, at the end. - These three calls were recorded from a real JVM. Your comparator never moves anything; it only answers with a sign.
Ordering in real systems
Every sorted screen, ranked list and priority queue in an app has a comparator behind it.
Watch it with an example: “Sort by” on a shopping app
The same four phones, re-sorted each time the shopper taps a different option. Each option is just a different Comparator.
- The search returns four phones in no particular order.
- “Price: low to high” uses
comparingInt(Phone::price). A and C cost the same; the stable sort keeps A before C, as they were. - “Rating” sorts by stars, highest first, using
reversed(). - “Price: high to low” with a rating tie-breaker: C (★4.6) now comes before A (★4.2). (Simplified: real apps sort in the database and add a unique ID as the last tie-breaker.)
1. “Sort by” menus
Shopping and travel apps offer “price low to high”, “rating”, “newest”. Each option is a Comparator (often with a tie-breaker like product ID so the order is stable across pages).
2. Rankings and leaderboards
Exam results, sales leaderboards and cricket tables sort by a main score with tie-breakers (net run rate, then wins), exactly a thenComparing chain.
3. Priority processing
Support tickets by severity then age, hospital triage by condition, delivery jobs by deadline: a PriorityQueue with a Comparator always hands out the most urgent item next.
4. Natural orders in value types
Types like LocalDate, BigDecimal and app version numbers implement Comparable, so they sort correctly in TreeMaps and reports without extra code.
Examples are simplified. Large lists are usually sorted by the database (ORDER BY) before reaching Java.
Exam cell: three printouts from one list
import java.util.*;
class Student implements Comparable<Student> {
final int roll;
final String name;
final int marks;
Student(int roll, String name, int marks) {
this.roll = roll;
this.name = name;
this.marks = marks;
}
int roll() { return roll; }
String name() { return name; }
int marks() { return marks; }
@Override
public int compareTo(Student o) { return Integer.compare(roll, o.roll); }
@Override
public String toString() { return String.format("%02d %-6s %d", roll, name, marks); }
static final Comparator<Student> BY_MERIT =
Comparator.comparingInt(Student::marks).reversed().thenComparing(Comparator.naturalOrder());
static final Comparator<Student> BY_NAME =
Comparator.comparing(Student::name, String.CASE_INSENSITIVE_ORDER);
}
public class Main {
static void print(String title, List<Student> list) {
System.out.println(title);
for (Student s : list) System.out.println(" " + s);
}
public static void main(String[] args) {
List<Student> students = new ArrayList<>(List.of(
new Student(21, "Om", 92), new Student(7, "Riya", 92),
new Student(12, "Kabir", 78), new Student(3, "zoya", 85)));
Collections.sort(students);
print("Register:", students);
students.sort(Student.BY_MERIT);
print("Merit list:", students);
students.sort(Student.BY_NAME);
print("Certificates:", students);
}
}implements Comparable<Student>The natural order: roll number.Collections.sort(students)uses it.Integer.compare(roll, o.roll)Safe comparison; no subtraction overflow.BY_MERITMarks high → low, ties broken by the natural order (roll).BY_NAMECase-insensitive, so “zoya” doesn't jump after capitals.
sort calls your compareTo
This trace is recorded from the real JVM. You never call compareTo yourself: watch Collections.sort call it on pairs of students (frames appear and disappear) and the list change order.
Worked examples: real problems, solved and traced in memory
Three sorting problems from real apps, using Comparable, a custom Comparator, and a comparator inside a PriorityQueue.
Trains in departure order
Problem statement
A station display lists trains by departure time (HHMM as an int). Give Train a natural order by departure and sort three trains.
12123 at 1715, 11007 at 0640, 12127 at 1015
0640 11007 1015 12127 1715 12123
Approach
Train implements Comparable<Train>;compareTousesInteger.compareon departure.Collections.sort(list)needs no extra argument.- Print the board.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Leaderboard: revenue high to low, then name
Problem statement
Sales reps: Riya ₹80k, Om ₹95k, Kabir ₹80k. Sort by revenue (highest first); equal revenue by name. Write the comparator by hand as a class.
Riya 80, Om 95, Kabir 80
Om ₹95k Kabir ₹80k Riya ₹80k
Approach
- A class
ByRevenue implements Comparator<Rep>. compare: compare revenues in reverse (Integer.compare(b, a)); if 0, compare names.list.sort(new ByRevenue()).
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Serve the most urgent ticket first
Problem statement
Tickets have a severity (1 = critical) and an arrival number. Serve by severity, and for equal severity, by earlier arrival.
(#1, sev 3), (#2, sev 1), (#3, sev 3), (#4, sev 1)
#2 #4 #1 #3
Approach
- A
PriorityQueue<int[]>with a comparator: severity first, then arrival. - Offer all tickets.
- Poll until empty: always the most urgent, oldest first.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 11.8 in one screen
- Comparable (java.lang):
compareTo(o)inside the class = one natural order. - Comparator (java.util):
compare(a, b)outside = any number of orders, even for classes you can't edit. - Result sign: negative (before), 0 (same), positive (after).
- Build comparators:
comparing,comparingInt,thenComparing,reversed,nullsFirst/Last,naturalOrder,reverseOrder,String.CASE_INSENSITIVE_ORDER. - Use
Integer.compare, never subtraction. Keep compareTo consistent with equals. - Used by
Collections.sort,List.sort,Arrays.sort, TreeSet, TreeMap, PriorityQueue. Object sort = stable TimSort.
Interview and viva questions
Comparable vs Comparator: when do you use which?
Implement Comparable when the class has one obvious, natural order (ID, date, version). Use Comparators for every other order, for orders that depend on the situation (a screen's “sort by” choice), and for classes you don't own. Many classes have both: a natural order plus named comparators.
What does “consistent with equals” mean?
That compareTo returns 0 exactly when equals returns true. If not, sorted collections disagree with hash collections: a TreeSet may treat two unequal objects as duplicates, or keep two equal ones. BigDecimal is a famous exception: 2.0 and 2.00 compare as 0 but aren't equal.
Can you sort with a lambda?
Yes. Comparator is a functional interface, so list.sort((a, b) -> Integer.compare(a.marks, b.marks)) works, as does list.sort(Comparator.comparingInt(Student::marks)). Lambdas are covered in Lesson 14.1.
What is “Comparison method violates its general contract”?
TimSort throws this IllegalArgumentException when it detects an inconsistent comparator, e.g. one that isn't transitive, uses random values, or overflows with subtraction. Fix the comparator so it's a proper total order.
How do you sort in descending order?
list.sort(Comparator.reverseOrder()) for natural order, cmp.reversed() for a custom comparator, or Collections.sort(list, Collections.reverseOrder()).
Practice problems
Write natural orders and comparators, chain tie-breakers and avoid overflow. The last three are business tasks.
Sort numbers descending
Sort [5, 1, 9, 3] from highest to lowest.
[5, 1, 9, 3]
[9, 5, 3, 1]
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Integer> l = new ArrayList<>(List.of(5, 1, 9, 3));
l.sort(Comparator.reverseOrder());
System.out.println(l);
}
}How it works: reverseOrder() flips Integer's natural order.
Sort words by length
Sort ["banana", "fig", "apple"] by length, shortest first.
["banana", "fig", "apple"]
[fig, apple, banana]
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> w = new ArrayList<>(List.of("banana", "fig", "apple"));
w.sort(Comparator.comparingInt(String::length));
System.out.println(w);
}
}How it works: comparingInt(String::length) builds a comparator from a key: the length of each word.
Comparable product by price
Make Product Comparable by price and sort three products.
Pen 10, Bag 450, Book 120
[Pen ₹10, Book ₹120, Bag ₹450]
Show solution and explanation
import java.util.*;
class Product implements Comparable<Product> {
String name; int price;
Product(String n, int p) { name = n; price = p; }
public int compareTo(Product o) { return Integer.compare(price, o.price); }
public String toString() { return name + " ₹" + price; }
}
public class Main {
public static void main(String[] args) {
List<Product> l = new ArrayList<>(List.of(new Product("Pen", 10), new Product("Bag", 450), new Product("Book", 120)));
Collections.sort(l);
System.out.println(l);
}
}How it works: once the class is Comparable, Collections.sort needs no comparator.
Names: case-insensitive, then by length
Sort ["om", "Riya", "kabir", "Om"] case-insensitively; for equal names, shorter first, then natural order.
["om", "Riya", "kabir", "Om"]
[kabir, Om, om, Riya]
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> n = new ArrayList<>(List.of("om", "Riya", "kabir", "Om"));
n.sort(String.CASE_INSENSITIVE_ORDER.thenComparing(Comparator.naturalOrder()));
System.out.println(n);
}
}How it works: "om" and "Om" tie case-insensitively; the natural order then puts "Om" (capital O = 79) before "om" (111).
Nulls last
Sort ["b", null, "a"] with nulls at the end.
["b", null, "a"]
[a, b, null]
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> l = new ArrayList<>(Arrays.asList("b", null, "a"));
l.sort(Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(l);
}
}How it works: without nullsLast, natural ordering would throw NullPointerException.
Delivery route by pin, then time
Deliveries (pin, time): (410210, 14), (400703, 11), (410210, 9). Sort by pin, then by time.
3 deliveries
400703 at 11:00 410210 at 9:00 410210 at 14:00
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
List<int[]> d = new ArrayList<>(List.of(new int[]{410210, 14}, new int[]{400703, 11}, new int[]{410210, 9}));
d.sort(Comparator.<int[]>comparingInt(x -> x[0]).thenComparingInt(x -> x[1]));
for (int[] x : d) System.out.println(x[0] + " at " + x[1] + ":00");
}
}How it works: the second key only matters when the pins are equal. The <int[]> hint tells javac the element type for the lambdas.
Top 2 rated restaurants
Restaurants with ratings: Chai Point 4.3, Dosa Hut 4.7, Vada Corner 4.5. Print the top 2 by rating.
3 restaurants
[Dosa Hut=4.7, Vada Corner=4.5]
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Double> rating = new HashMap<>(Map.of("Chai Point", 4.3, "Dosa Hut", 4.7, "Vada Corner", 4.5));
List<Map.Entry<String, Double>> list = new ArrayList<>(rating.entrySet());
list.sort(Map.Entry.<String, Double>comparingByValue().reversed());
System.out.println(list.subList(0, 2));
}
}How it works: map entries can be sorted with Map.Entry.comparingByValue(); reversed for highest first.
Safe comparator for large amounts
Sort transaction amounts in paise [9_000_000_000L, -5L, 300L] ascending, safely.
three long amounts
[-5, 300, 9000000000]
Show solution and explanation
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Long> paise = new ArrayList<>(List.of(9_000_000_000L, -5L, 300L));
paise.sort(Long::compare);
System.out.println(paise);
}
}How it works: Long.compare never overflows; a subtraction cast to int would give wrong signs for values this large.
Real-world scenario questions (asked in company interviews)
Sorting questions in interviews check whether your orders are correct, stable and maintainable.
Products jump between pages
Search results are sorted by price and shown 20 per page. Users report the same product appearing on page 1 and page 2, and some never appearing. Many products share the same price. What's wrong?
The interviewer is checking: tie-breakers and deterministic ordering.
Show a strong answer
- Ties by price have no defined order, so each page request (often a separate database query) may order equal-priced products differently.
- Add a unique tie-breaker:
comparing(Product::price).thenComparing(Product::id)in Java, orORDER BY price, idin SQL. - Now every request produces the same total order, so pages don't overlap.
Avoid: blaming caching. The order itself is ambiguous.
Large transactions sorted wrongly
A report sorts transactions by amount in paise using (a, b) -> (int) (a.paise - b.paise). Small amounts sort fine; crore-rupee transfers appear in random places. Why?
The interviewer is checking: overflow and casting in comparators.
Show a strong answer
- The difference of two large
longvalues cast tointoverflows, so the sign can be wrong. - Use
Long.compare(a.paise, b.paise)orComparator.comparingLong(Txn::paise). - Add a test with very large and negative values; a broken comparator can even make TimSort throw.
Avoid: converting to double; precision issues follow.
Points table for a cricket league
Rank teams by points (high first); if equal, by net run rate (high first); if still equal, by team name. Write the comparator.
The interviewer is checking: multi-level comparators.
Show a strong answer
Comparator.comparingInt(Team::points).reversed().thenComparing(Comparator.comparingDouble(Team::nrr).reversed()).thenComparing(Team::name).- Note that
reversed()on a chain reverses everything before it, so reverse each key's comparator separately, as above. - Keep it as a named constant (e.g.
Team.TABLE_ORDER) and unit-test tie cases.
Avoid: a long hand-written if/else compare method; it's easy to get a branch wrong.
Should Order implement Comparable?
A teammate wants Order implements Comparable<Order> comparing by total amount, “because we often sort by amount”. Do you agree?
The interviewer is checking: what a natural order should be.
Show a strong answer
- Amount isn't a natural, unique order: two different orders can have the same amount, so compareTo wouldn't be consistent with equals, and TreeSet<Order> would drop orders.
- If anything, the natural order should be the unique order ID or creation time. Sorting by amount should be a named Comparator (
Order.BY_AMOUNT). - This keeps sorted collections and reports correct and explicit.
Avoid: making the most frequently used sort the natural order just for convenience.