Java Comparable vs Comparator — Sorting Objects
Sorting objects in Java requires defining an order. Java provides two interfaces: Comparable for a class’s natural ordering, and Comparator for custom, external ordering.
Comparable — Natural Ordering
A class implements Comparable<T> to define its default sort order:
public class Student implements Comparable<Student> {
private String name;
private double gpa;
@Override
public int compareTo(Student other) {
return Double.compare(this.gpa, other.gpa); // ascending by GPA
}
}
The Contract
compareTo() returns:
- Negative —
thiscomes beforeother - Zero —
thisequalsother - Positive —
thiscomes afterother
List<Student> students = new ArrayList<>(...);
Collections.sort(students); // uses compareTo()
students.sort(null); // same — null means natural order
Classes with Natural Ordering
| Class | Natural Order |
|---|---|
String |
Lexicographic (alphabetical) |
Integer, Double, etc. |
Numeric ascending |
LocalDate, LocalDateTime |
Chronological |
Enum |
Declaration order |
Comparator — Custom Ordering
Comparator<T> defines an external, reusable ordering:
Comparator<Student> byName = new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return a.getName().compareTo(b.getName());
}
};
students.sort(byName);
Lambda Syntax (Java 8+)
students.sort((a, b) -> a.getName().compareTo(b.getName()));
Method Reference with Comparator.comparing()
students.sort(Comparator.comparing(Student::getName));
students.sort(Comparator.comparingDouble(Student::getGpa));
students.sort(Comparator.comparingInt(Student::getAge));
Comparable vs Comparator
| Feature | Comparable<T> |
Comparator<T> |
|---|---|---|
| Method | compareTo(T) |
compare(T, T) |
| Location | Inside the class | External (separate class/lambda) |
| Number of orderings | One (natural) | Unlimited |
| Modifies original class | ✅ | ❌ |
| Functional interface | ❌ | ✅ (lambda-friendly) |
| Use when | There’s one obvious ordering | You need multiple orderings |
Chaining Comparators
// Sort by GPA descending, then by name ascending
Comparator<Student> comparator = Comparator
.comparingDouble(Student::getGpa).reversed()
.thenComparing(Student::getName);
students.sort(comparator);
Available Chaining Methods
Comparator.comparing(keyExtractor) // sort by key
.thenComparing(keyExtractor) // secondary sort
.thenComparingInt(intExtractor) // secondary by int
.thenComparingDouble(dblExtractor) // secondary by double
.reversed() // reverse the entire chain
Handling Nulls
// Nulls first, then sort by name
Comparator<Student> nullSafe = Comparator.nullsFirst(
Comparator.comparing(Student::getName)
);
// Nulls last
Comparator<Student> nullsLast = Comparator.nullsLast(
Comparator.comparing(Student::getName)
);
Null-safe Field Comparison
// Student's name might be null
Comparator<Student> byName = Comparator.comparing(
Student::getName,
Comparator.nullsLast(Comparator.naturalOrder())
);
Reversing Order
// Reverse natural order
Comparator<String> descending = Comparator.reverseOrder();
names.sort(descending);
// Reverse a specific comparator
Comparator<Student> byGpaDesc = Comparator
.comparingDouble(Student::getGpa)
.reversed();
Complete Example
public record Employee(String name, String department, double salary)
implements Comparable<Employee> {
@Override
public int compareTo(Employee other) {
return Double.compare(this.salary, other.salary); // natural: by salary
}
}
List<Employee> employees = List.of(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Marketing", 72000),
new Employee("Charlie", "Engineering", 88000),
new Employee("Diana", "Marketing", 72000),
new Employee("Eve", "Engineering", 95000)
);
// Natural order (by salary)
employees.stream().sorted().forEach(System.out::println);
// By department, then salary descending, then name
Comparator<Employee> complex = Comparator
.comparing(Employee::department)
.thenComparingDouble(Employee::salary).reversed()
.thenComparing(Employee::name);
employees.stream().sorted(complex).forEach(System.out::println);
Using with TreeSet and TreeMap
// TreeSet with natural ordering (Comparable)
Set<Student> byGpa = new TreeSet<>(); // uses Student.compareTo()
// TreeSet with custom ordering (Comparator)
Set<Student> byName = new TreeSet<>(Comparator.comparing(Student::getName));
// TreeMap sorted by key
Map<String, Integer> sorted = new TreeMap<>(); // natural String order
Map<String, Integer> reversed = new TreeMap<>(Comparator.reverseOrder());
Warning:
TreeSet/TreeMapusescompareTo()orcompare()for equality. Ifcompare(a, b) == 0, the tree considersaandbequal — even ifa.equals(b)isfalse.
Writing a Good compareTo()
// ✅ CORRECT — using built-in compare methods
@Override
public int compareTo(Student other) {
int result = this.name.compareTo(other.name);
if (result != 0) return result;
return Double.compare(this.gpa, other.gpa);
}
// ❌ BAD — integer overflow risk
@Override
public int compareTo(Student other) {
return this.age - other.age; // overflow if values are extreme!
}
// ✅ CORRECT — use Integer.compare for int fields
@Override
public int compareTo(Student other) {
return Integer.compare(this.age, other.age);
}
Consistency with equals()
It’s strongly recommended that compareTo() is consistent with equals():
a.compareTo(b) == 0 should mean a.equals(b) == true
If they’re inconsistent, sorted collections (TreeSet, TreeMap) may behave unexpectedly — e.g., a TreeSet might consider two non-equal objects as duplicates.
Key Takeaways
Comparable= one natural ordering, defined inside the class (compareTo).Comparator= custom/multiple orderings, defined externally (compare).- Use
Comparator.comparing()+thenComparing()for clean, readable chains. - Use
nullsFirst()/nullsLast()for null-safe sorting. - Use
Integer.compare(),Double.compare()— never subtract for comparison. - Keep
compareTo()consistent withequals()for predictable behaviour in sorted collections.
What’s Next?
Next: Introduction to Generics — type safety, generic classes, methods, and type erasure.