Skip to content
Dev Dump

Comparator vs Comparable

“Understanding the difference between Comparable and Comparator is fundamental to implementing proper object ordering in Java collections. Comparable provides natural ordering, while Comparator offers flexible, external comparison logic.”

Object comparison in Java is the mechanism for determining the relative ordering of objects. This is essential for sorting collections, implementing search algorithms, and maintaining ordered data structures like TreeSet and TreeMap.

  1. Comparable: Natural ordering defined within the class itself
  2. Comparator: External comparison logic that can be customized
AspectComparableComparator
LocationInside the class being comparedExternal to the class
MethodcompareTo(T other)compare(T o1, T o2)
Packagejava.langjava.util
Sorting OrdersSingle natural orderMultiple custom orders
FlexibilityLimited to one orderingUnlimited orderings
ModificationRequires changing the classNo class modification needed
UsageCollections.sort(list)Collections.sort(list, comparator)
public interface Comparable<T> {
/**
* Compares this object with the specified object for order.
* Returns:
* - negative integer if this < other
* - zero if this == other
* - positive integer if this > other
*/
int compareTo(T other);
}
public class Person implements Comparable<Person> {
private String name;
private int age;
private double salary;
public Person(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
@Override
public int compareTo(Person other) {
// Primary comparison: by age
return Integer.compare(this.age, other.age);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age &&
Double.compare(person.salary, salary) == 0 &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age, salary);
}
// Getters and toString()
public String getName() { return name; }
public int getAge() { return age; }
public double getSalary() { return salary; }
@Override
public String toString() {
return String.format("Person{name='%s', age=%d, salary=%.2f}", name, age, salary);
}
}
public class ComparableSortingExample {
public void demonstrateComparableSorting() {
List<Person> people = Arrays.asList(
new Person("Alice", 25, 50000),
new Person("Bob", 30, 60000),
new Person("Charlie", 25, 55000),
new Person("Diana", 28, 52000)
);
// Natural sorting using Comparable
Collections.sort(people);
// Or using List.sort() (Java 8+)
people.sort(null); // null means natural ordering
// Display sorted results
people.forEach(System.out::println);
}
}
public interface Comparator<T> {
/**
* Compares two objects for order.
* Returns:
* - negative integer if o1 < o2
* - zero if o1 == o2
* - positive integer if o1 > o2
*/
int compare(T o1, T o2);
// Default methods for chaining and composition
default Comparator<T> reversed();
default Comparator<T> thenComparing(Comparator<? super T> other);
default <U> Comparator<T> thenComparing(Function<? super T, ? extends U> keyExtractor);
default <U extends Comparable<? super U>> Comparator<T> thenComparing(Function<? super T, ? extends U> keyExtractor);
}
public class PersonComparators {
// Compare by name
public static class NameComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName());
}
}
// Compare by age
public static class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return Integer.compare(p1.getAge(), p2.getAge());
}
}
// Compare by salary (descending)
public static class SalaryComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return Double.compare(p2.getSalary(), p1.getSalary()); // Descending
}
}
}
public class ComparatorSortingExample {
public void demonstrateComparatorSorting() {
List<Person> people = Arrays.asList(
new Person("Alice", 25, 50000),
new Person("Bob", 30, 60000),
new Person("Charlie", 25, 55000),
new Person("Diana", 28, 52000)
);
// Sort by name
Collections.sort(people, new PersonComparators.NameComparator());
System.out.println("Sorted by name:");
people.forEach(System.out::println);
// Sort by age
Collections.sort(people, new PersonComparators.AgeComparator());
System.out.println("\nSorted by age:");
people.forEach(System.out::println);
// Sort by salary (descending)
Collections.sort(people, new PersonComparators.SalaryComparator());
System.out.println("\nSorted by salary (descending):");
people.forEach(System.out::println);
}
}
public class LambdaComparatorExample {
public void demonstrateLambdaComparators() {
List<Person> people = Arrays.asList(
new Person("Alice", 25, 50000),
new Person("Bob", 30, 60000),
new Person("Charlie", 25, 55000)
);
// Lambda-based comparators
Comparator<Person> nameComparator = (p1, p2) -> p1.getName().compareTo(p2.getName());
Comparator<Person> ageComparator = (p1, p2) -> Integer.compare(p1.getAge(), p2.getAge());
Comparator<Person> salaryComparator = (p1, p2) -> Double.compare(p2.getSalary(), p1.getSalary());
// Sort by name
people.sort(nameComparator);
// Sort by age
people.sort(ageComparator);
// Sort by salary (descending)
people.sort(salaryComparator);
}
}
public class ChainingComparatorExample {
public void demonstrateChaining() {
List<Person> people = Arrays.asList(
new Person("Alice", 25, 50000),
new Person("Bob", 25, 60000),
new Person("Charlie", 30, 55000),
new Person("Diana", 30, 52000)
);
// Complex sorting: age (ascending), then salary (descending), then name
Comparator<Person> complexComparator = Comparator
.comparing(Person::getAge) // Primary: age ascending
.thenComparing(Person::getSalary, Comparator.reverseOrder()) // Secondary: salary descending
.thenComparing(Person::getName); // Tertiary: name ascending
people.sort(complexComparator);
people.forEach(System.out::println);
}
}
public class NullSafeComparatorExample {
public void demonstrateNullSafeComparison() {
List<String> names = Arrays.asList("Alice", null, "Bob", null, "Charlie");
// Null-safe comparator
Comparator<String> nullSafeComparator = Comparator.nullsFirst(String::compareTo);
// Sort with nulls first
names.sort(nullSafeComparator);
System.out.println("Nulls first: " + names);
// Sort with nulls last
Comparator<String> nullsLastComparator = Comparator.nullsLast(String::compareTo);
names.sort(nullsLastComparator);
System.out.println("Nulls last: " + names);
}
}
public class CustomComparatorExample {
public void demonstrateCustomComparator() {
List<Person> people = Arrays.asList(
new Person("Alice", 25, 50000),
new Person("Bob", 30, 60000),
new Person("Charlie", 25, 55000)
);
// Custom comparator: prioritize people with salary > 55000
Comparator<Person> customComparator = (p1, p2) -> {
boolean p1HighSalary = p1.getSalary() > 55000;
boolean p2HighSalary = p2.getSalary() > 55000;
if (p1HighSalary && !p2HighSalary) return -1;
if (!p1HighSalary && p2HighSalary) return 1;
// If both have same salary category, sort by age
return Integer.compare(p1.getAge(), p2.getAge());
};
people.sort(customComparator);
people.forEach(System.out::println);
}
}
  • The class has a natural ordering that makes sense
  • You want to control the ordering from within the class
  • The ordering is consistent and unlikely to change
  • You need default sorting behavior
  • You need multiple different ordering options
  • You want to sort objects without modifying their class
  • The ordering is context-dependent or temporary
  • You need flexible, reusable comparison logic
  • You’re working with third-party classes you can’t modify
// ❌ Bad - Inconsistent comparison
@Override
public int compareTo(Person other) {
if (this.age < other.age) return -1;
if (this.age > other.age) return 1;
if (this.salary < other.salary) return -1;
if (this.salary > other.salary) return 1;
return 0;
}
// ✅ Good - Consistent comparison
@Override
public int compareTo(Person other) {
int ageComparison = Integer.compare(this.age, other.age);
if (ageComparison != 0) return ageComparison;
return Double.compare(this.salary, other.salary);
}
// ❌ Bad - Potential NullPointerException
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name); // May throw NPE
}
// ✅ Good - Null-safe comparison
@Override
public int compareTo(Person other) {
if (this.name == null && other.name == null) return 0;
if (this.name == null) return -1;
if (other.name == null) return 1;
return this.name.compareTo(other.name);
}
// ❌ Bad - Breaks transitivity
@Override
public int compareTo(Person other) {
// This can break transitivity if age and salary have different scales
return (this.age + this.salary) - (other.age + other.salary);
}
// ✅ Good - Maintains transitivity
@Override
public int compareTo(Person other) {
int ageComparison = Integer.compare(this.age, other.age);
if (ageComparison != 0) return ageComparison;
return Double.compare(this.salary, other.salary);
}