Comparable<T> defines a type's natural ordering from the inside: the
class implements it and supplies a single compareTo(T other) method. It lives
in java.lang. Comparator<T> defines an external, custom ordering
in a separate object via compare(T a, T b); it lives in java.util.
Comparable<T> |
Comparator<T> |
|
|---|---|---|
| Package | java.lang |
java.util |
| Method | int compareTo(T o) |
int compare(T a, T b) |
| Where | implemented by the class | a separate object |
| Orderings | one (the natural one) | as many as you like |
| Modifies class? | yes | no |
Rule of thumb: use Comparable for the one obvious ordering a type is
(numbers by value, strings alphabetically), and Comparator for everything
else — especially for types you can't edit.
Implement Comparable<YourType> and override compareTo to return a
negative int, zero, or a positive int when this is less than, equal to, or
greater than the argument.
class Person implements Comparable<Person> {
String name; int age;
@Override public int compareTo(Person other) {
return Integer.compare(this.age, other.age); // natural order = by age
}
}
Once a type is Comparable, it works automatically with Collections.sort,
Arrays.sort, TreeSet, TreeMap, and Stream.sorted() without a
comparator argument. Rule of thumb: parameterize the interface
(Comparable<Person>), never the raw Comparable.
The return value's sign is what matters, not its magnitude: negative if the first argument sorts before the second, zero if equal, positive if after. The relation must be:
- Antisymmetric —
sgn(a.compareTo(b)) == -sgn(b.compareTo(a)). - Transitive — if
a > bandb > c, thena > c. - Consistent — equal inputs always give equal results.
Integer.compare(3, 5); // negative (any negative int)
Integer.compare(5, 5); // 0
Integer.compare(7, 2); // positive
Breaking the contract (e.g. an ordering that isn't transitive) can make
Arrays.sort throw IllegalArgumentException: Comparison method violates its general contract!. Rule of thumb: define order, then never break transitivity.
Subtraction can overflow. If a is large positive and b is large
negative, a - b wraps past Integer.MAX_VALUE and becomes negative — so a
value that should sort after sorts before, silently corrupting the order.
int a = Integer.MAX_VALUE, b = -1;
System.out.println(a - b); // -2147483648 (wrong sign!)
System.out.println(Integer.compare(a, b)); // positive (correct)
The fix is Integer.compare(a, b) (or Long.compare, Double.compare),
which is overflow-safe and reads clearly. Rule of thumb: never subtract to
compare — always use the compare static helpers.
Comparator.comparing(keyExtractor) takes a function that pulls a
Comparable sort key out of each element and returns a comparator that
orders by that key — no boilerplate compare body needed.
List<Person> people = ...;
people.sort(Comparator.comparing(Person::getName)); // by name
people.sort(Comparator.comparing(Person::getAge)); // by age
The extracted key (getName, getAge) must itself be Comparable, or you pass
a second argument: comparing(Person::getCity, String.CASE_INSENSITIVE_ORDER).
Rule of thumb: prefer comparing(...) with a method reference over a hand-written
lambda — it's shorter and reads like the sort you mean.
Chain thenComparing after comparing. It acts as a tie-breaker: the
next comparator is consulted only when the previous one returns zero.
people.sort(
Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparingInt(Person::getAge) // primitive, no boxing
);
You can chain as many levels as you need; each runs only on ties from the level
above. Rule of thumb: list your sort keys in priority order, comparing first
and thenComparing for each subsequent tie-breaker.
Call .reversed() on any comparator to flip its direction, or use
Comparator.reverseOrder() for the reverse of natural ordering.
people.sort(Comparator.comparing(Person::getAge).reversed()); // oldest first
List<Integer> nums = ...;
nums.sort(Comparator.reverseOrder()); // descending naturals
nums.sort(Collections.reverseOrder()); // same idea, older API
Watch the placement with chains: reversed() reverses the entire
comparator built so far, not just the last thenComparing. To reverse only one
field, reverse that key: thenComparing(Person::getAge, reverseOrder()). Rule of
thumb: parenthesize your chain so you know exactly what reversed() flips.
Comparator.naturalOrder() returns a comparator that uses each element's own
compareTo (its natural ordering); reverseOrder() returns the inverse.
Both work on any Comparable type.
List<String> s = new ArrayList<>(List.of("b", "a", "c"));
s.sort(Comparator.naturalOrder()); // [a, b, c]
s.sort(Comparator.reverseOrder()); // [c, b, a]
They're most useful as arguments to other methods — e.g. as the key
comparator in comparing(...) or the inner comparator of nullsFirst(...).
Rule of thumb: reach for naturalOrder()/reverseOrder() instead of writing
(a, b) -> a.compareTo(b) by hand.
A plain comparator throws NullPointerException on null elements. Wrap it
with Comparator.nullsFirst(...) or nullsLast(...) to decide where
nulls land.
List<String> s = Arrays.asList("b", null, "a");
s.sort(Comparator.nullsFirst(Comparator.naturalOrder())); // [null, a, b]
s.sort(Comparator.nullsLast(Comparator.naturalOrder())); // [a, b, null]
It also guards a null key in a chain:
comparing(Person::getName, nullsLast(naturalOrder())). Rule of thumb: if a
field can be null, wrap its comparator in nullsFirst/nullsLast rather than
hoping the data is clean.
The generic comparing(...) returns a key of type Comparable, which means an
int key gets autoboxed to Integer on every comparison. The primitive
specializations comparingInt, comparingLong, and comparingDouble
take a primitive-returning extractor and avoid that boxing.
// boxes each age to Integer
people.sort(Comparator.comparing(Person::getAge));
// no boxing — faster in hot paths
people.sort(Comparator.comparingInt(Person::getAge));
Same applies to the chained form: prefer thenComparingInt. Rule of thumb: when
the sort key is a primitive, use the comparingInt/Long/Double variant.
Both sort a List in place. Collections.sort(list) is the older static
helper; list.sort(comparator) is a default method added to List in Java
8 and is the modern idiom. Functionally they do the same thing.
Collections.sort(list); // natural order
Collections.sort(list, comparator); // custom
list.sort(null); // null == natural order
list.sort(Comparator.comparing(Person::getAge));
Collections.sort actually delegates to list.sort under the hood. Passing
null as the comparator means "use natural ordering" (the elements must be
Comparable). Rule of thumb: prefer list.sort(...) — it's the method on the
object you already have.
Arrays.sort sorts an array. For an object array you can pass a
comparator; for primitive arrays you cannot (only natural ascending order is
supported).
Integer[] boxed = {3, 1, 2};
Arrays.sort(boxed, Comparator.reverseOrder()); // [3, 2, 1] — needs Integer[]
int[] prims = {3, 1, 2};
Arrays.sort(prims); // [1, 2, 3] only — no comparator
To sort primitives descending you must box to Integer[], or sort ascending and
reverse manually. Rule of thumb: comparators need object arrays; primitive
Arrays.sort is natural-order only.
Stream.sorted() sorts by natural ordering (elements must be Comparable);
Stream.sorted(comparator) sorts by a custom order. It's an intermediate
operation that returns a new, sorted stream.
List<Person> byAge = people.stream()
.sorted(Comparator.comparingInt(Person::getAge))
.collect(Collectors.toList());
Unlike list.sort, this does not mutate the source — it produces a new
ordering downstream. sorted() is also stateful: it must buffer all elements
before emitting any. Rule of thumb: use stream sorted when you're already in a
pipeline and want a new collection, not an in-place sort.
TreeMap and TreeSet keep elements sorted. By default they use the
elements' natural ordering (Comparable), but you can pass a Comparator to the
constructor to control the order — and to allow non-Comparable types.
TreeSet<String> ci = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
ci.add("Bravo"); ci.add("alpha"); // sorted case-insensitively
TreeMap<Person, String> m =
new TreeMap<>(Comparator.comparing(Person::getName));
The comparator is fixed at construction and used for every ordering and
lookup operation. Rule of thumb: supply a comparator when the natural order isn't
what you want — or when the key type isn't Comparable at all.
A PriorityQueue is a min-heap by default — poll() returns the
smallest element per natural ordering. Pass a comparator to change what
"smallest" means, e.g. to build a max-heap.
PriorityQueue<Integer> min = new PriorityQueue<>(); // 1 first
PriorityQueue<Integer> max =
new PriorityQueue<>(Comparator.reverseOrder()); // largest first
PriorityQueue<Person> byAge =
new PriorityQueue<>(Comparator.comparingInt(Person::getAge));
The heap only guarantees the head is the min/max — iterating the queue does
not yield sorted order. Rule of thumb: pass reverseOrder() for a max-heap,
and only trust peek/poll, never iteration order.
An ordering is consistent with equals when a.compareTo(b) == 0 exactly
when a.equals(b) is true. It's strongly recommended but not required — and
violating it breaks sorted collections like TreeSet/TreeMap, which judge
equality by compareTo, not equals.
// BigDecimal is the classic inconsistency:
new BigDecimal("1.0").equals(new BigDecimal("1.00")); // false
new BigDecimal("1.0").compareTo(new BigDecimal("1.00")); // 0 -> "equal"
So a TreeSet<BigDecimal> treats 1.0 and 1.00 as the same element, while
a HashSet treats them as distinct. Rule of thumb: keep compareTo consistent
with equals unless you have a specific reason, and document it when you don't.
A TreeSet is backed by a balanced tree, so it locates and de-duplicates
elements purely by comparison: if compareTo/compare returns 0, the
element is considered a duplicate and is rejected — equals and hashCode
are never consulted.
// comparator looks only at age:
TreeSet<Person> set = new TreeSet<>(Comparator.comparingInt(Person::getAge));
set.add(new Person("Ada", 30));
set.add(new Person("Bob", 30)); // NOT added — same age -> compare == 0
System.out.println(set.size()); // 1
This surprises people: two objects that aren't equals can still collide in a
TreeSet if the comparator rates them equal. Rule of thumb: make the
TreeSet/TreeMap comparator total enough that only truly-equal elements
compare to 0.
A stable sort preserves the relative order of elements that compare as equal. This matters when you sort by multiple keys in passes, or want ties to keep their original sequence.
// sort by city, then (stably) by name keeps city groups intact
people.sort(Comparator.comparing(Person::getName));
people.sort(Comparator.comparing(Person::getCity)); // ties stay name-ordered
Collections.sort, List.sort, and Arrays.sort on objects (TimSort) are
guaranteed stable. Arrays.sort on primitives uses a dual-pivot
quicksort and is not stable (irrelevant for primitives, since equal ints are
indistinguishable). Rule of thumb: rely on stability only for object sorts.
Comparator<T> is a functional interface (one abstract method, compare),
so you can write it as a lambda or, more cleanly, build it with the factory
methods and method references.
// verbose lambda
Comparator<Person> c1 = (a, b) -> a.getName().compareTo(b.getName());
// factory + method reference — preferred
Comparator<Person> c2 = Comparator.comparing(Person::getName);
The factory form is shorter, harder to get wrong (no manual compareTo), and
composes with thenComparing/reversed. Rule of thumb: reserve raw (a, b) ->
lambdas for genuinely custom logic; use Comparator.comparing(...) for the
common "sort by a field" case.
Comparators are typically stateless and thread-safe, so the idiom is to build
one once as a static final constant and reuse it everywhere rather than
re-creating it on every sort.
public static final Comparator<Person> BY_NAME_THEN_AGE =
Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparingInt(Person::getAge);
people.sort(BY_NAME_THEN_AGE);
A named constant documents the ordering, avoids rebuilding the chain in hot
loops, and can be shared across TreeSet, PriorityQueue, and sort calls.
Rule of thumb: name and cache the comparators you use more than once.
More Collections interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.