Comparable and Comparator Interfaces in Java
You add four people to a TreeSet and print it — only three come out. “Tom Smith” vanished without an exception or a warning: the comparator looked only at last names, returned 0 for the two Smiths, and to a TreeSet that means “this element is already here.” Bugs like this disappear once you understand how Comparable and Comparator work.
Comparable and Comparator are the two Java interfaces for comparing objects. Comparable<T> (package java.lang) defines the natural ordering of a class: the class itself implements compareTo(T o). Comparator<T> (package java.util) is an external comparison rule: compare(T o1, T o2) is implemented in a separate class or a lambda, and one class can have as many comparators as you need. Both methods return a negative number, zero or a positive number.
Comparable interface in Java and the compareTo() method
The Comparable interface in Java is implemented by classes whose objects have a natural ordering: numbers are compared by value, strings alphabetically by character code, dates chronologically. It has a single method, int compareTo(T o), which compares the current object (this) with the argument. String, Integer, LocalDate, BigDecimal and every enum already implement it.
How to read the result of compareTo():
- a negative number — the current object goes before the argument;
- a positive number — it goes after the argument;
- zero — the objects are equal for this ordering (which does not necessarily mean
equals()returnstrue, see The compareTo contract).
The type parameter in implements Comparable<Person> matters: the method becomes compareTo(Person other) and needs no cast. With the raw type Comparable the signature would be compareTo(Object o), and a wrong argument type would only show up at runtime as a ClassCastException. Type parameters in general are explained in Java generics.
Here is how to implement Comparable in Java — a Person class whose natural ordering is by age. The examples below use this class:
import java.util.Objects;
public class Person implements Comparable<Person> {
private final String firstName;
private final String lastName;
private final int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age
&& Objects.equals(firstName, person.firstName)
&& Objects.equals(lastName, person.lastName);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName, age);
}
@Override
public String toString() {
return firstName + " " + lastName + ", " + age;
}
} A TreeSet created without a comparator orders its elements with compareTo():
import java.util.SortedSet;
import java.util.TreeSet;
public class ComparePersonExample {
public static void main(String[] args) {
SortedSet<Person> set = new TreeSet<>();
set.add(new Person("John", "Smith", 36));
set.add(new Person("Mary", "Jones", 23));
set.add(new Person("Kate", "Brown", 34));
set.add(new Person("Tom", "Smith", 25));
set.forEach(System.out::println);
}
} Output:
Mary Jones, 23
Tom Smith, 25
Kate Brown, 34
John Smith, 36 If the class does not implement Comparable and no comparator is passed, the TreeSet fails on the very first add() with a ClassCastException: on Java 8 the message is Cat cannot be cast to java.lang.Comparable, on Java 11+ class Cat cannot be cast to class java.lang.Comparable. The compiler does not catch this, because new TreeSet<>() accepts any element type. The TreeSet navigation methods (first(), headSet(), subSet()) belong to the SortedSet and TreeSet lesson.
How compareTo compares strings
String.compareTo() compares strings character by character by their UTF-16 codes and returns the difference between the first pair of characters that differ. So all uppercase Latin letters (65–90) come before lowercase ones (97–122), and accented letters such as “É” (U+00C9, code 201) come after every unaccented letter. What does this code print?
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
public class StringOrderExample {
public static void main(String[] args) {
List<String> words = new ArrayList<>(Arrays.asList("peach", "Éclair", "apple", "Banana"));
words.sort(null); // natural ordering
System.out.println(words);
words.sort(String.CASE_INSENSITIVE_ORDER); // ignore case
System.out.println(words);
words.sort(Collator.getInstance(Locale.ENGLISH)); // language rules
System.out.println(words);
}
} Output:
[Banana, apple, peach, Éclair]
[apple, Banana, peach, Éclair]
[apple, Banana, Éclair, peach] Even CASE_INSENSITIVE_ORDER leaves “Éclair” at the end: it only folds upper and lower case, and “É” stays code 201. Dictionary order comes only from a Collator: it implements Comparator and treats “É” as a variant of “E”. Case-insensitive sorting of plain ASCII names is shown in Sorting a list: list.sort().
Comparator interface in Java and the compare() method
The Comparator interface in Java is used when you cannot or do not want to change the class (for example String or a library class), or when objects must be sorted in different ways: by last name today, by age tomorrow. Its main method is int compare(T o1, T o2): a negative result puts o1 before o2, a positive one after, and zero means the objects are equal for this ordering.
The classic way is a separate class that implements the comparator:
import java.util.Comparator;
public class PersonComparator implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
return o1.getLastName().compareTo(o2.getLastName());
}
} The comparator is passed to the TreeSet constructor and replaces the natural ordering by age:
import java.util.SortedSet;
import java.util.TreeSet;
public class PersonComparatorExample {
public static void main(String[] args) {
SortedSet<Person> set = new TreeSet<>(new PersonComparator());
set.add(new Person("John", "Smith", 36));
set.add(new Person("Mary", "Jones", 23));
set.add(new Person("Kate", "Brown", 34));
boolean added = set.add(new Person("Tom", "Smith", 25));
System.out.println("Tom added: " + added + ", size: " + set.size());
set.forEach(System.out::println);
}
} Output:
Tom added: false, size: 3
Kate Brown, 34
Mary Jones, 23
John Smith, 36 Where did the second Smith go
TreeSet and the keys of a TreeMap detect duplicates with the comparator, not with equals(): if compare() returns 0, the new element counts as already present and is not added. “Tom Smith” and “John Smith” differ by equals(), but the comparator sees only the last name. To keep both, add a second criterion to the comparator — thenComparing() from the section below.
Java Comparable vs Comparator: the difference
The main difference: Comparable defines one natural ordering inside the class itself, while Comparator defines any number of external sort rules without touching the original class. If the order is obvious and unique (numbers, dates, codes), implement Comparable; for everything else, write a comparator.
| Property | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Method | int compareTo(T o) — compares this with o | int compare(T o1, T o2) — compares the two arguments |
| Where it is implemented | In the compared class itself | In a separate class, an anonymous class or a lambda |
| Number of orderings | One — the natural ordering | As many as you need |
| Needs access to the class source | Yes | No — works for String and third-party classes |
| How it is plugged in | Collections.sort(list), list.sort(null), new TreeSet<>() | list.sort(cmp), Arrays.sort(arr, cmp), new TreeSet<>(cmp) |
| Ready-made helper methods | None, only compareTo() | comparing(), thenComparing(), reversed(), nullsFirst() and more (Java 8) |
@FunctionalInterface annotation | No | Yes |
The two approaches work together: the Person class implements Comparable by age and can still be sorted by last name with a comparator. A comparator that is passed in always takes priority over the natural ordering.
Comparator.comparing, comparingInt, thenComparing, reversed
Since Java 8 a comparator is rarely written as a separate class: you assemble it from the static and default methods of Comparator. Comparator.comparing(Person::getLastName) compares by last name, thenComparing() adds the next criterion for ties, and reversed() flips the order. The full list is in the Comparator Javadoc.
| Method | What it does | Edge case |
|---|---|---|
static comparing(Function keyExtractor) | Compares by the key the function returns (the key must be Comparable) | A null key — NullPointerException during comparison |
static comparing(Function keyExtractor, Comparator keyCmp) | Compares the keys with the given comparator | For example comparing(Person::getLastName, Comparator.reverseOrder()) |
static comparingInt / comparingLong / comparingDouble | Compares by a primitive key | No boxing into Integer; uses Integer.compare() inside, so no overflow |
default thenComparing(...), thenComparingInt(...) | Next criterion when the previous one returned 0 | Can be chained any number of times |
default reversed() | Reverse order | Reverses the whole chain it is called on, not only the last criterion |
static naturalOrder() / reverseOrder() | Natural Comparable order / its reverse | Comparator.reverseOrder() does the same as Collections.reverseOrder() |
static nullsFirst(cmp) / nullsLast(cmp) | Puts null at the start / end, compares the rest with cmp | Without them, null in a sorted list — NullPointerException |
A Java 8 Comparator example: sort a list by last name, break ties by age, then sort by age in descending order:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class PersonComparingExample {
public static void main(String[] args) {
List<Person> people = new ArrayList<>(Arrays.asList(
new Person("John", "Smith", 36),
new Person("Mary", "Jones", 23),
new Person("Kate", "Brown", 34),
new Person("Tom", "Smith", 25)));
people.sort(Comparator.comparing(Person::getLastName)
.thenComparingInt(Person::getAge));
people.forEach(System.out::println);
System.out.println("---");
people.sort(Comparator.comparingInt(Person::getAge).reversed());
people.forEach(System.out::println);
}
} Output:
Kate Brown, 34
Mary Jones, 23
Tom Smith, 25
John Smith, 36
---
John Smith, 36
Kate Brown, 34
Tom Smith, 25
Mary Jones, 23 Both Smiths are back: when the last names are equal, the second criterion decides. The same comparator passed to new TreeSet<>(...) would keep both of them as well.
Sorting a list that contains null
Natural ordering cannot compare null: "Anna".compareTo(null) throws NullPointerException. Wrapping the comparator in nullsFirst() or nullsLast() solves this without a null check in every comparison (fragment, imports java.util.*):
List<String> names = new ArrayList<>(Arrays.asList("Oliver", null, "Anna"));
names.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(names); // Output: [null, Anna, Oliver]
names.sort(Comparator.naturalOrder()); // NullPointerException Comparator with a lambda and a method reference
Comparator is a functional interface, so instead of the PersonComparator class a lambda expression (p1, p2) -> p1.getLastName().compareTo(p2.getLastName()) is enough, or even shorter, Comparator.comparing(Person::getLastName) with a method reference. All three lines below define the same order (fragment):
Comparator<Person> byClass = new PersonComparator();
Comparator<Person> byLambda = (p1, p2) -> p1.getLastName().compareTo(p2.getLastName());
Comparator<Person> byRef = Comparator.comparing(Person::getLastName); What trips people up most often is a lambda inside a chain. This code does not compile:
people.sort(Comparator.comparing(p -> p.getLastName()).reversed());
// error: cannot find symbol
// symbol: method getLastName()
// location: variable p of type Object The compiler infers the type of p from the comparing(...) call itself, and that call has no target type: .reversed() is called on its result, so the type expected by people.sort() never reaches the lambda. p becomes Object. Three fixes: a method reference Comparator.comparing(Person::getLastName).reversed(), an explicit parameter type comparing((Person p) -> p.getLastName()), or an explicit type argument Comparator.<Person, String>comparing(p -> p.getLastName()). Without .reversed(), comparing(p -> p.getLastName()) compiles fine.
The compareTo contract: overflow and consistency with equals
The compareTo() and compare() methods must follow a contract: the sign of a.compareTo(b) is the opposite of the sign of b.compareTo(a), the order is transitive (if a > b and b > c, then a > c), and elements that compare as equal compare the same way with any third element. If the contract is broken, sorting gives a wrong result or fails with IllegalArgumentException: Comparison method violates its general contract!.
Why you should not write return a - b
Subtraction is the most common way to break the contract. For small numbers a - b has the right sign, but when the int overflows the sign flips:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class OverflowExample {
public static void main(String[] args) {
Comparator<Integer> bad = (a, b) -> a - b;
Comparator<Integer> good = Integer::compare;
System.out.println(bad.compare(Integer.MIN_VALUE, 1));
System.out.println(good.compare(Integer.MIN_VALUE, 1));
List<Integer> nums = new ArrayList<>(Arrays.asList(1, Integer.MIN_VALUE));
nums.sort(bad);
System.out.println(nums);
nums.sort(good);
System.out.println(nums);
}
} Output:
2147483647
-1
[1, -2147483648]
[-2147483648, 1] Integer.MIN_VALUE - 1 overflows into Integer.MAX_VALUE, so the “bad” comparator decides that minus two billion is greater than one, and the list stays unsorted.
Compare numbers without subtraction
Instead of return this.age - other.age write Integer.compare(this.age, other.age), for long and double use Long.compare() and Double.compare(), and in chains use Comparator.comparingInt(). An age will never overflow, but the subtraction habit will one day hit IDs, amounts in cents or negative values — and tests with small numbers will not catch it.
Consistency of compareTo with equals
An ordering is called consistent with equals if a.compareTo(b) == 0 exactly when a.equals(b). The Java documentation strongly recommends this but does not require it. Breaking it does not affect sorting a list, but it changes how TreeSet and TreeMap behave: they find and drop duplicates by compareTo()/compare(), while a HashSet uses equals() and hashCode(). The classic example from the standard library is BigDecimal (fragment, imports java.math.BigDecimal and java.util.*):
List<BigDecimal> values = Arrays.asList(new BigDecimal("1.0"), new BigDecimal("1.00"));
Set<BigDecimal> hashSet = new HashSet<>(values);
Set<BigDecimal> treeSet = new TreeSet<>(values);
System.out.println(hashSet.size()); // Output: 2 - equals() takes the scale into account
System.out.println(treeSet.size()); // Output: 1 - compareTo() returned 0
System.out.println(treeSet); // Output: [1.0] Our Person is not consistent with equals() either: two different people of the same age are equal for compareTo(), and a TreeSet without a comparator would reject the second one — just like the two Smiths. If objects will live in a TreeSet or serve as TreeMap keys, include in the comparison every field that equals() uses. How equals() and hashCode() work in hash-based collections is covered in The hashCode() and equals() contract and the equals() method in Java.
Where Comparable and Comparator are used
Comparable and Comparator are used by every sorted collection and sorting method in Java: Collections.sort(), List.sort(), Arrays.sort(), TreeSet, TreeMap, PriorityQueue, Stream.sorted(), Collections.max(). The rule is the same everywhere: if a comparator is passed, it is used; if not (or null is passed), the natural Comparable ordering applies.
| Where | Without a comparator | With a comparator | Note |
|---|---|---|---|
| Sorting a list | Collections.sort(list), list.sort(null) | list.sort(cmp), Collections.sort(list, cmp) | Sorts in place; see list.sort() |
| Sorting an array | Arrays.sort(arr) | Arrays.sort(arr, cmp) | A comparator works only with object arrays: there is no such overload for int[], you need Integer[] (see Arrays.sort() and wrapper classes) |
TreeSet | new TreeSet<>() | new TreeSet<>(cmp) | Duplicates are decided by the comparison, not by equals() |
TreeMap (key order) | new TreeMap<>() | new TreeMap<>(cmp) | Same rule for keys |
PriorityQueue | new PriorityQueue<>() — the smallest element comes out first | new PriorityQueue<>(cmp) | The iterator does not return elements in sorted order |
| Maximum and minimum | Collections.max(coll), Collections.min(coll) | Collections.max(coll, cmp), stream.max(cmp) | An empty collection — NoSuchElementException from Collections.max() |
| Stream API | stream.sorted() | stream.sorted(cmp) | The source collection is not changed; see Stream API |
List.sort(), Collections.sort() and Arrays.sort() for object arrays are stable: elements for which the comparator returns 0 keep their original relative order. So a multi-field sort can be built either with a thenComparing() chain or with several sorts in a row, from the last criterion to the first. How a sorted collection differs from an ordered one is explained in Ordered vs sorted collections in Java.
Frequently Asked Questions
Does compareTo() have to return -1, 0 or 1?
No, only the sign matters. String.compareTo(), for example, returns the difference of character codes: "apple".compareTo("cherry") gives minus 2. That is why the check a.compareTo(b) == 1 is a bug; write a.compareTo(b) > 0 instead.
Why is Comparator a functional interface if it also declares equals()?
The boolean equals(Object obj) method in Comparator redeclares a public method of Object, and such methods are not counted as abstract methods of an interface. The other methods (reversed(), thenComparing(), comparing()) are default or static. The only abstract method is compare(), so a comparator can be written as a lambda.
How are enum constants compared?
Every enum implements Comparable, and its natural ordering is the order in which the constants are declared (by ordinal()). compareTo() in Enum is final, so you cannot override it; for another order use a comparator, for example Comparator.comparing(Enum::name).
How do I sort objects in descending order?
For natural ordering use list.sort(Comparator.reverseOrder()); for a comparator add .reversed(): Comparator.comparingInt(Person::getAge).reversed(). To reverse only one criterion in a chain, pass it as the second argument: thenComparing(Person::getAge, Comparator.reverseOrder()). An example with output is in Comparator.comparing, comparingInt, thenComparing, reversed.
Comments