Skip to content

Comparable & Comparator Interview Questions & Answers

20 questions Updated 2026-06-20 Share:

Java Comparable and Comparator interview questions — natural vs custom ordering, compareTo vs compare, comparator chaining and reversal, sorting collections, and consistency with equals.

Read the in-depth guideJava Comparable vs Comparator — Sorting, Chaining & the Pitfalls(opens in new tab)
20 of 20

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:

  • Antisymmetricsgn(a.compareTo(b)) == -sgn(b.compareTo(a)).
  • Transitive — if a > b and b > c, then a > 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 ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel