Contents

Implementing Comparable<T> adds a natural ordering to your class — the order that makes intuitive sense for that type. String sorts alphabetically, Integer sorts numerically, LocalDate sorts chronologically. When you implement Comparable, your objects can be sorted by Collections.sort() and Arrays.sort() without any extra instructions, and they can be placed in TreeSet and used as TreeMap keys without supplying a comparator.

import java.util.*; public class Employee implements Comparable<Employee> { private final String lastName; private final String firstName; private final int salary; public Employee(String lastName, String firstName, int salary) { this.lastName = lastName; this.firstName = firstName; this.salary = salary; } // Natural ordering: alphabetical by last name, then first name @Override public int compareTo(Employee other) { int cmp = this.lastName.compareTo(other.lastName); if (cmp != 0) return cmp; return this.firstName.compareTo(other.firstName); } @Override public String toString() { return lastName + ", " + firstName + " ($" + salary + ")"; } } public class Main { public static void main(String[] args) { List<Employee> employees = new ArrayList<>(List.of( new Employee("Smith", "Charlie", 75_000), new Employee("Adams", "Zara", 90_000), new Employee("Smith", "Alice", 80_000), new Employee("Johnson", "Bob", 70_000) )); Collections.sort(employees); // uses compareTo — no comparator needed employees.forEach(System.out::println); // Adams, Zara ($90000) // Johnson, Bob ($70000) // Smith, Alice ($80000) // Smith, Charlie ($75000) } } The natural ordering defined by Comparable should be consistent with equals(): if a.compareTo(b) == 0 then a.equals(b) should return true. Violating this causes confusing behaviour in sorted sets and maps — an element may appear to be "missing" even though contains() returns false for it.

The compareTo() method must return a negative integer, zero, or a positive integer as the receiver is less than, equal to, or greater than the argument. Three rules govern correct implementation:

// WRONG — integer subtraction overflows for large values public int compareTo(MyNumber other) { return this.value - other.value; // OVERFLOW BUG if values differ by > Integer.MAX_VALUE } // CORRECT — use Integer.compare for int fields public int compareTo(MyNumber other) { return Integer.compare(this.value, other.value); // safe, no overflow } // CORRECT — use Double.compare for double fields public int compareTo(Price other) { return Double.compare(this.amount, other.amount); // safe, handles NaN } // CORRECT — use String.compareTo for strings (already implements Comparable) public int compareTo(Person other) { return this.name.compareTo(other.name); } // CORRECT — multi-field with early exit public int compareTo(Event other) { int cmp = this.date.compareTo(other.date); // LocalDate if (cmp != 0) return cmp; return this.title.compareTo(other.title); }

A Comparator<T> is a standalone comparison strategy — separate from the class being compared. It is a functional interface with one abstract method, compare(T a, T b), so it can be expressed as a lambda or method reference. You use a Comparator when:

import java.util.*; List<String> words = new ArrayList<>(List.of("Banana", "apple", "Cherry", "date")); // Lambda comparator — case-insensitive sort words.sort((a, b) -> a.compareToIgnoreCase(b)); System.out.println(words); // [apple, Banana, Cherry, date] // Method reference comparator words.sort(String::compareToIgnoreCase); System.out.println(words); // [apple, Banana, Cherry, date] // Sorting by string length words.sort((a, b) -> Integer.compare(a.length(), b.length())); System.out.println(words); // [date, apple, Banana, Cherry] // Sorting integers in reverse List<Integer> nums = new ArrayList<>(List.of(5, 2, 8, 1, 9, 3)); nums.sort((a, b) -> Integer.compare(b, a)); // reversed by swapping args System.out.println(nums); // [9, 8, 5, 3, 2, 1]

Java 8 introduced static factory methods on Comparator that build comparators declaratively from key extractors. Comparator.comparing() takes a function that extracts a Comparable key from the object, and returns a Comparator that sorts by that key. thenComparing() adds a secondary sort criterion when the primary is equal.

import java.util.*; import java.util.stream.*; record Product(String name, String category, double price, int stock) {} List<Product> products = List.of( new Product("Laptop", "Electronics", 999.99, 50), new Product("Phone", "Electronics", 699.99, 200), new Product("Desk", "Furniture", 299.99, 30), new Product("Chair", "Furniture", 149.99, 75), new Product("Monitor", "Electronics", 399.99, 120) ); // Sort by category, then by price ascending Comparator<Product> byCategoryThenPrice = Comparator.comparing(Product::category) .thenComparingDouble(Product::price); products.stream() .sorted(byCategoryThenPrice) .forEach(p -> System.out.printf("%-12s %-15s $%.2f%n", p.name(), p.category(), p.price())); // Chair Furniture $149.99 // Desk Furniture $299.99 // Phone Electronics $699.99 // Monitor Electronics $399.99 // Laptop Electronics $999.99 // Sort by stock descending, then by name ascending Comparator<Product> byStockDescThenName = Comparator.comparingInt(Product::stock).reversed() .thenComparing(Product::name); products.stream() .sorted(byStockDescThenName) .map(p -> p.name() + " (" + p.stock() + ")") .forEach(System.out::println); // Phone (200) // Monitor (120) // Chair (75) // Laptop (50) // Desk (30) Prefer Comparator.comparingInt(), comparingLong(), and comparingDouble() over comparing() when the key is a primitive — they avoid boxing and run faster. Similarly use thenComparingInt() etc. in chains.

Comparator provides built-in methods to reverse an ordering and to handle null values safely — which would otherwise throw NullPointerException during sorting.

import java.util.*; import java.util.stream.*; List<String> names = new ArrayList<>( Arrays.asList("Charlie", null, "Alice", null, "Bob") ); // WRONG — throws NullPointerException // names.sort(Comparator.naturalOrder()); // CORRECT — nulls first, then alphabetical names.sort(Comparator.nullsFirst(Comparator.naturalOrder())); System.out.println(names); // [null, null, Alice, Bob, Charlie] // CORRECT — nulls last, then reverse alphabetical names.sort(Comparator.nullsLast(Comparator.reverseOrder())); System.out.println(names); // [Charlie, Bob, Alice, null, null] // Reversing any comparator with .reversed() Comparator<String> byLength = Comparator.comparingInt(String::length); names.removeIf(Objects::isNull); names.sort(byLength.reversed()); System.out.println(names); // [Charlie, Alice, Bob] (longest first) // Min/max with a custom comparator Optional<String> shortest = names.stream() .min(Comparator.comparingInt(String::length)); System.out.println(shortest.orElse("none")); // Bob

Both Collections.sort() and List.sort() modify the list in place. Stream.sorted() returns a new sorted stream without touching the original collection. All three accept either no comparator (uses natural ordering via Comparable) or an explicit Comparator.

import java.util.*; import java.util.stream.*; record Person(String name, int age) {} List<Person> people = new ArrayList<>(List.of( new Person("Alice", 30), new Person("Bob", 25), new Person("Carol", 30), new Person("Dave", 25) )); // In-place sort — modifies the list people.sort(Comparator.comparingInt(Person::age).thenComparing(Person::name)); people.forEach(System.out::println); // Person[name=Bob, age=25] // Person[name=Dave, age=25] // Person[name=Alice, age=30] // Person[name=Carol, age=30] // Stream sorted — original list unchanged, returns new sorted stream List<Person> byName = people.stream() .sorted(Comparator.comparing(Person::name)) .collect(Collectors.toList()); System.out.println(byName); // [Person[name=Alice, age=30], Person[name=Bob, age=25], // Person[name=Carol, age=30], Person[name=Dave, age=25]] // min / max via Comparator Person youngest = people.stream() .min(Comparator.comparingInt(Person::age)) .orElseThrow(); System.out.println(youngest); // Person[name=Bob, age=25] // Arrays.sort with Comparator Person[] arr = people.toArray(Person[]::new); Arrays.sort(arr, Comparator.comparing(Person::name).reversed()); System.out.println(Arrays.toString(arr));

TreeSet and TreeMap keep their elements/keys in sorted order. By default they use the natural ordering of the elements (via Comparable). You can override this by passing a Comparator to the constructor — which is required when the elements do not implement Comparable or when you want a different order.

import java.util.*; // TreeSet with natural ordering (String implements Comparable) TreeSet<String> alphabetical = new TreeSet<>(); alphabetical.addAll(List.of("banana", "apple", "cherry", "date")); System.out.println(alphabetical); // [apple, banana, cherry, date] // TreeSet with custom comparator — reverse alphabetical TreeSet<String> reversed = new TreeSet<>(Comparator.reverseOrder()); reversed.addAll(List.of("banana", "apple", "cherry", "date")); System.out.println(reversed); // [date, cherry, banana, apple] // TreeSet with length comparator (secondary by natural order for ties) TreeSet<String> byLength = new TreeSet<>( Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()) ); byLength.addAll(List.of("banana", "fig", "apple", "kiwi", "date")); System.out.println(byLength); // [fig, date, kiwi, apple, banana] // TreeMap sorted by key length TreeMap<String, Integer> wordCount = new TreeMap<>( Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()) ); wordCount.put("hello", 5); wordCount.put("hi", 2); wordCount.put("hey", 3); System.out.println(wordCount); // {hi=2, hey=3, hello=5} When you supply a Comparator to a TreeSet, the set uses it for all operations including contains(), remove(), and add(). If the comparator considers two elements equal (compare() == 0), the set treats them as the same element — even if their equals() methods return false. Always ensure your comparator is consistent with equals() to avoid losing elements silently.
ScenarioUse
One obvious "natural" sort order for your class (alphabetical, chronological, numerical) Comparable
You want TreeSet/TreeMap to work without extra code Comparable
Multiple sort orders for the same class Comparator
Sorting a class you don't own (String, Integer, etc.) Comparator
Sort order changes at runtime (user chooses column) Comparator
You need null-safe sorting Comparator.nullsFirst/nullsLast
Complex multi-key sort with readable code Comparator.comparing().thenComparing()
You can implement both. A class that implements Comparable for its natural ordering can still be sorted with a Comparator for a different order. The comparator always takes precedence when explicitly provided.