Java Notes
Deep dive into Comparable and Comparator interfaces in Java — natural ordering, custom sorting, lambda expressions, method references, and practical comparisons with examples.
Introduction
When you work with collections of objects in Java, you often need to sort them. Java provides two interfaces for defining sort order:
Comparable<T>— Defines the natural ordering of a class (built into the class itself)Comparator<T>— Defines external/custom ordering (separate from the class)
Think of it this way:
Comparablesays: "I know how to compare myself with another object of my type"Comparatorsays: "I know how to compare any two objects of a given type"
The Comparator Interface
Definition
package java.util;
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
// Default and static methods (Java 8+)
default Comparator<T> reversed() { ... }
default Comparator<T> thenComparing(Comparator<? super T> other) { ... }
static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor) { ... }
static <T> Comparator<T> naturalOrder() { ... }
static <T> Comparator<T> reverseOrder() { ... }
static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) { ... }
static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) { ... }
}Basic Usage — Sort by GPA
public class GpaComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return Double.compare(s2.getGpa(), s1.getGpa()); // Descending
}
}
// Usage
List<Student> students = getStudents();
Collections.sort(students, new GpaComparator());Anonymous Class
Collections.sort(students, new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
return s1.getName().compareTo(s2.getName());
}
});Lambda Expression (Java 8+)
Since Comparator is a @FunctionalInterface:
// Sort by name
Collections.sort(students, (s1, s2) -> s1.getName().compareTo(s2.getName()));
// Even shorter with method reference
students.sort(Comparator.comparing(Student::getName));Method References with Comparator.comparing()
// Sort by age ascending
students.sort(Comparator.comparing(Student::getAge));
// Sort by GPA descending
students.sort(Comparator.comparing(Student::getGpa).reversed());
// Sort by name, then by age (chained)
students.sort(Comparator.comparing(Student::getName)
.thenComparing(Student::getAge));
// Sort by GPA descending, then by name ascending
students.sort(Comparator.comparing(Student::getGpa).reversed()
.thenComparing(Student::getName));Comparator Utility Methods (Java 8+)
Comparator.comparing()
// Extract key and compare by natural order
Comparator<Student> byName = Comparator.comparing(Student::getName);
Comparator<Student> byAge = Comparator.comparingInt(Student::getAge);
Comparator<Student> byGpa = Comparator.comparingDouble(Student::getGpa);reversed()
Comparator<Student> byAgeDesc = Comparator.comparing(Student::getAge).reversed();thenComparing()
// Multi-level sort: by age, then by name, then by GPA
Comparator<Student> multiSort = Comparator.comparing(Student::getAge)
.thenComparing(Student::getName)
.thenComparingDouble(Student::getGpa);Handling Nulls
// Nulls first, then sort by name
Comparator<Student> nullSafe = Comparator.nullsFirst(
Comparator.comparing(Student::getName)
);
// Null fields
Comparator<Student> nullFieldSafe = Comparator.comparing(
Student::getName,
Comparator.nullsLast(Comparator.naturalOrder())
);Complete Comparison: Comparable vs Comparator
| Aspect | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(T o) | compare(T o1, T o2) |
| Modifies class? | Yes (class must implement it) | No (external) |
| Number of orderings | One (natural ordering) | Unlimited |
| Functional interface? | No (has only one abstract method but not annotated) | Yes (@FunctionalInterface) |
| Lambda support | No | Yes |
| Used by | Collections.sort(list), Arrays.sort(), TreeSet, TreeMap | Collections.sort(list, comp), list.sort(comp) |
| Null handling | Must handle yourself | nullsFirst(), nullsLast() utilities |
Using with Collections
With TreeSet
// Natural ordering (Comparable)
TreeSet<Student> byAge = new TreeSet<>(); // Uses compareTo()
// Custom ordering (Comparator)
TreeSet<Student> byName = new TreeSet<>(Comparator.comparing(Student::getName));With TreeMap
// Keys sorted by natural order
TreeMap<String, Integer> map1 = new TreeMap<>();
// Keys sorted by custom comparator (reverse)
TreeMap<String, Integer> map2 = new TreeMap<>(Comparator.reverseOrder());With Arrays.sort()
Student[] arr = { new Student("Alice", 22, 3.8), new Student("Bob", 20, 3.5) };
Arrays.sort(arr); // Uses Comparable (natural ordering)
Arrays.sort(arr, Comparator.comparing(Student::getName)); // CustomWith Streams
List<Student> sorted = students.stream()
.sorted(Comparator.comparing(Student::getGpa).reversed())
.collect(Collectors.toList());Real-World Example: Multi-Level Sorting
public class Employee {
private String department;
private String name;
private double salary;
private LocalDate joiningDate;
// Constructor, getters, toString...
}
public class SortingDemo {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Engineering", "Alice", 95000, LocalDate.of(2020, 3, 15)),
new Employee("Engineering", "Bob", 92000, LocalDate.of(2019, 7, 1)),
new Employee("Marketing", "Charlie", 85000, LocalDate.of(2021, 1, 10)),
new Employee("Engineering", "Diana", 95000, LocalDate.of(2018, 11, 20)),
new Employee("Marketing", "Eve", 88000, LocalDate.of(2020, 6, 5))
);
// Sort by: department → salary (desc) → joining date
Comparator<Employee> complexSort = Comparator
.comparing(Employee::getDepartment)
.thenComparing(Employee::getSalary, Comparator.reverseOrder())
.thenComparing(Employee::getJoiningDate);
employees.sort(complexSort);
employees.forEach(System.out::println);
}
}Output:
Consistency with equals()
The Comparable documentation strongly recommends that compareTo() be consistent with equals():
// Inconsistent example — BigDecimal
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
a.equals(b); // false (different scale)
a.compareTo(b); // 0 (mathematically equal)
// This causes problems with TreeSet:
TreeSet<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(a);
treeSet.add(b);
System.out.println(treeSet.size()); // 1 (considers them equal via compareTo)
HashSet<BigDecimal> hashSet = new HashSet<>();
hashSet.add(a);
hashSet.add(b);
System.out.println(hashSet.size()); // 2 (considers them different via equals)Common Mistakes
1. Subtraction Trick Overflow
// DANGEROUS — can overflow for large values!
public int compareTo(Student other) {
return this.age - other.age; // Integer overflow if values are extreme
}
// SAFE — use Integer.compare()
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}2. Forgetting Transitivity
// BROKEN — not transitive
public int compare(String a, String b) {
return a.length() == b.length() ? 0 : (a.length() > b.length() ? 1 : -1);
}
// If we also consider alphabetical order in some cases, transitivity may break3. Not Handling Null
// Use Comparator utilities for null safety
Comparator<Employee> safe = Comparator.comparing(
Employee::getDepartment,
Comparator.nullsLast(Comparator.naturalOrder())
);4. Inconsistent compareTo and equals with TreeSet
// If compareTo returns 0, TreeSet considers elements equal (won't add both)
// even if equals() returns falseInterview Questions
Q1: When should you use Comparable vs Comparator?
A: Use Comparable when there's a single, obvious natural ordering for your class (e.g., Students sorted by ID). Use Comparator when you need multiple sort orders or can't modify the class.
Q2: Can a class implement Comparable and also be sorted with a Comparator?
A: Yes. Collections.sort(list) uses Comparable; Collections.sort(list, comparator) uses the Comparator, which takes precedence over natural ordering.
Q3: Why is the subtraction trick dangerous in compareTo()?
A: Because a - b can overflow if a is a large positive and b is a large negative (or vice versa). Use Integer.compare(a, b) instead.
Q4: What happens if compareTo() is inconsistent with equals()?
A: Sorted collections (TreeSet, TreeMap) use compareTo for equality checks. If compareTo() returns 0 but equals() returns false, TreeSet won't store both elements, but HashSet will.
Q5: How do you sort a list in reverse order?
A: Several ways:
Collections.sort(list, Comparator.reverseOrder())list.sort(Comparator.comparing(Key::method).reversed())Collections.reverse(list)after sorting
Q6: What is a natural ordering?
A: The default sort order defined by the class's compareTo() method. For String it's lexicographic, for Integer it's numeric, for Date it's chronological.
Q7: Can Comparator be used as a lambda?
A: Yes, because it's a @FunctionalInterface. Example: list.sort((a, b) -> a.getName().compareTo(b.getName())) or list.sort(Comparator.comparing(Person::getName)).
Q8: Can a class implement both Comparable and use Comparator?
A: Yes. A class can implement Comparable for its natural ordering and you can still create separate Comparator objects for alternative sort orders. When you call Collections.sort(list), it uses Comparable. When you call Collections.sort(list, comparator), it uses the Comparator, ignoring the natural order.
Q9: What happens if compareTo() is inconsistent with equals()?
A: TreeSet and TreeMap use compareTo() for element equality. If compareTo() returns 0 but equals() returns false, TreeSet will treat them as duplicates (won't add both), but HashSet will treat them as different objects. This inconsistency can cause confusing bugs.
Q10: How do you sort by multiple fields using Comparator?
A: Use Comparator.comparing().thenComparing() chain: Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName).thenComparingInt(Person::getAge). Each thenComparing is used only when the previous comparison returns 0 (tie).
Q11: What is the purpose of Comparator.naturalOrder() and reverseOrder()?
A: Comparator.naturalOrder() returns a comparator that uses the element's natural ordering (Comparable). Comparator.reverseOrder() returns the reverse of natural order. These are useful in streams and as method references: list.sort(Comparator.reverseOrder()).
Q12: Can Comparator be used with primitive arrays?
A: No directly. Arrays.sort(int[]) uses dual-pivot quicksort and doesn't accept a Comparator. You must use wrapper arrays (Integer[]) or streams: Arrays.stream(arr).boxed().sorted(comparator).mapToInt(Integer::intValue).toArray().
Summary
| Feature | Use When |
|---|---|
Comparable | Class has one obvious natural ordering; you own the class |
Comparator | Need multiple orderings; can't modify the class; one-off sort |
Comparator.comparing() | Clean, readable key-based comparisons |
thenComparing() | Multi-level sorting |
reversed() | Descending order |
nullsFirst()/nullsLast() | Collections may contain null |
Master these patterns and you can sort any collection in any order with clean, readable code.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Comparable vs Comparator in Java.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, collections, framework, comparable
Related Java Master Course Topics