Sorting Custom Objects in Java — Comparable, Comparator, Comparator.comparing()
Fast recall
Quick revision
Core answer
compareTo; used by Arrays.sort(arr) and Collections.sort(list) with no extra argument; one ordering per class. (2) Pass a `ComparatorArrays.sort(arr, cmp) and list.sort(cmp); multiple orderings per class. Modern Java (8+): use `Comparator.comparing(Person::getAge)` factory methods for clean, type-safe comparators. Chain with `.thenComparing(Person::getName)` for multi-key sort. Reverse with `.reversed()`. Implementing Comparable: class Person implements Comparable { public int compareTo(Person o) { return Integer.compare(this.age, o.age); } } . Implementing Comparator (lambda): (a, b) -> Integer.compare(a.age, b.age) — verbose; prefer Comparator.comparingInt(Person::getAge). For null handling: Comparator.nullsFirst(...) / nullsLast(...). For descending: Comparator.reverseOrder() or .reversed(). Compare with Integer subtraction trick (a.age - b.age) is dangerous: integer overflow if values are large opposite signs. Use Integer.compare(a, b) instead. Chained comparison example: sort by lastName ascending, then firstName ascending, then age descending: Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName).thenComparing(Comparator.comparingInt(Person::getAge).reversed()). Stability: TimSort (Java's default for object sort) is stable — equal elements retain their original order. Useful for multi-pass sorting. Don't write `compareTo` that violates the contract — must be consistent with equals (recommended), reflexive, antisymmetric, transitive. Buggy comparators (e.g., returning random values) cause IllegalArgumentException from TimSort.Whether the candidate knows Comparable vs Comparator, can use Comparator.comparing() chains, handle null and reverse ordering, and avoid common bugs (subtraction overflow, contract violations)
Using `a.age - b.age` (overflow risk). Returning -1/0/1 inconsistently. Forgetting `.reversed()` is on the comparator, not the class. Mixing up Comparable's compareTo with Comparator's compare
Discuss the Comparator.comparing chain idioms, address Integer.compare for overflow safety, mention TimSort stability, contract requirements (consistent with equals), and natural vs external ordering
Interview-ready guide
Interview answer
Two ways to sort custom objects
1. Comparable
- Class implements Comparable
- Override
compareTo(other) - Used by
Collections.sort(list)with no extra arg - One ordering per class
- For obvious natural order (Date, Integer, String)
2. Comparator
- Pass to
list.sort(comparator)orCollections.sort(list, comparator) - Multiple orderings per class
- Use for context-specific sorts
- Use for third-party classes you can't modify
Java 8+ idiom — Comparator.comparing
list.sort(Comparator.comparing(Person::getName));
list.sort(Comparator.comparingInt(Person::getAge)); // no boxingChaining for multi-key sort
list.sort(
Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparingInt(Person::getAge)
);Reversing
Comparator.comparingInt(Person::getAge).reversed()
Comparator.comparing(Person::getName, Comparator.reverseOrder())Mixed direction
Comparator.comparing(Person::getName)
.thenComparing(Comparator.comparingInt(Person::getAge).reversed())Null handling
Comparator.nullsFirst(Comparator.comparing(Person::getName))
Comparator.comparing(Person::getName, Comparator.nullsLast(Comparator.naturalOrder()))Common bugs
1. Integer subtraction overflow
a.value - b.valueoverflows if values are extreme- Use
Integer.compare(a.value, b.value) - Same for Long, Double, etc.
2. Comparable contract violations
- Must be reflexive, antisymmetric, transitive
- Recommended: consistent with equals
- TimSort throws IllegalArgumentException on violations
3. .reversed() is on the Comparator
- Not on the method reference
Comparator.comparingInt(Person::getAge).reversed()✔Person::getAge.reversed()✘ (doesn't compile)
4. compareTo vs compare confusion
- Comparable.compareTo(other) — 1 param, this vs other
- Comparator.compare(a, b) — 2 params
5. Null handling
- Default comparators throw NPE on null
- Use nullsFirst / nullsLast
6. Modifying during sort
- Compared fields must be stable
- Concurrent modification → unpredictable
7. Wrong primitive comparator
- comparingInt avoids boxing
- comparing boxes int to Integer
8. == vs .equals in compareTo
- For Strings, use .compareTo() or .equals(), never ==
Comparable contract
- Reflexive: a.compareTo(a) == 0
- Antisymmetric: sgn(a.compareTo(b)) == -sgn(b.compareTo(a))
- Transitive: a > b and b > c implies a > c
- Recommended: consistent with equals (a.compareTo(b) == 0 iff a.equals(b))
Stability — TimSort is stable
- Equal elements retain original order
- Multi-pass sort works:
- Sort by age first
- Then sort by department — within department, age order preserved
- More common: chain comparators in one pass
Real-world examples
Employees by salary desc, then name asc
employees.sort(
Comparator.comparingDouble(Employee::getSalary).reversed()
.thenComparing(Employee::getName)
);Strings by length, then alphabetically
list.sort(
Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder())
);Top 10 highest salary
employees.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(10)
.toList();TreeSet / TreeMap caveat
- Use compareTo (or comparator) for membership, NOT equals
- If compareTo returns 0 for non-equal objects, they're treated as duplicates
- Inconsistency between compareTo and equals causes hard-to-find bugs
- E.g., HashSet with two equal-by-id but unequal-by-equals objects works fine; TreeSet treats them as one
For interviews
- Show Comparable for natural order
- Show Comparator.comparing chain for external
- Mention .thenComparing, .reversed, nullsFirst/Last
- Address Integer.compare for overflow safety
- Mention TimSort stability
Recommendation: Use `Comparablea - b to avoid integer overflow when values are extreme. For null handling, wrap with `Comparator.nullsFirst(...)` or `Comparator.nullsLast(...)`. The Comparable contract must be reflexive, antisymmetric, and transitive, and ideally consistent with `equals` — inconsistency causes weird behavior in TreeSet/TreeMap. TimSort (Java's default for object sort) is stable, so multi-pass sorting works, but prefer chaining comparators in one pass. Avoid common bugs: subtraction overflow, forgetting `.reversed()` is on Comparator, mixing up `compareTo` (one param) and `compare` (two params), and modifying objects during sort. The mental model: Comparable = one natural order baked into the class; Comparator = pluggable orderings via factory + chain. For complex orderings, build them declaratively with Comparator.comparing(...).thenComparing(...).reversed() — the chain reads top-to-bottom and is far cleaner than handwritten lambdas.
Sorting Custom Objects in Java — Comparable, Comparator, Comparator.comparing()
Direct Answer
Java offers two main ways to sort custom objects: (1) Implement Comparable<T> — defines the natural order for the class via compareTo; used by Arrays.sort(arr) and Collections.sort(list) with no extra argument; one ordering per class. (2) Pass a Comparator<T> — defines an external order; used by Arrays.sort(arr, cmp) and list.sort(cmp); multiple orderings per class. Modern Java (8+): use Comparator.comparing(Person::getAge) factory methods for clean, type-safe comparators. Chain with .thenComparing(Person::getName) for multi-key sort. Reverse with .reversed(). Implementing Comparable: class Person implements Comparable<Person> { public int compareTo(Person o) { return Integer.compare(this.age, o.age); } }. Implementing Comparator (lambda): (a, b) -> Integer.compare(a.age, b.age) — verbose; prefer Comparator.comparingInt(Person::getAge). For null handling: Comparator.nullsFirst(...) / nullsLast(...). For descending: Comparator.reverseOrder() or .reversed(). Compare with Integer subtraction trick (a.age - b.age) is dangerous: integer overflow if values are large opposite signs. Use Integer.compare(a, b) instead. Chained comparison example: sort by lastName ascending, then firstName ascending, then age descending: Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName).thenComparing(Comparator.comparingInt(Person::getAge).reversed()). Stability: TimSort (Java's default for object sort) is stable — equal elements retain their original order. Useful for multi-pass sorting. Don't write compareTo that violates the contract — must be consistent with equals (recommended), reflexive, antisymmetric, transitive. Buggy comparators (e.g., returning random values) cause IllegalArgumentException from TimSort.
Deep Dive & Architecture
TimSort is a hybrid stable sorting algorithm combining merge sort and insertion sort, designed by Tim Peters for Python in 2002 and adopted by Java since version 7 for sorting object arrays (Arrays.sort(Object[]), Collections.sort).
It's stable, adaptive (exploits existing order in the input), runs in O(n log n) worst case and O(n) on already-sorted data, with O(n) auxiliary space. Why Java uses it: (1) Stable — essential for multi-key sorts and predictable behavior with custom comparators; (2) Adaptive — real-world data often has runs of sorted elements; TimSort exploits them; (3) Worst-case O(n log n) — unlike quicksort which can degrade to O(n²); (4) Cache-friendly merging — better performance than naive merge sort. How it works: (1) Divide the array into 'runs' — maximal sorted (or reverse-sorted) subsequences; reverse-sorted runs are flipped in place. (2) Extend short runs to a 'minrun' length (typically 32-64) using insertion sort — fast on small data. (3) Merge runs using a stack-based strategy that maintains invariants ensuring balanced merge sizes; uses galloping mode when one run consistently dominates the other (jumps ahead by exponential strides). For primitive arrays (int[], double[]), Java uses Dual-Pivot Quicksort instead because stability is meaningless and primitive comparisons are cheap. Performance: ~O(n log n) typical; O(n) on already-sorted, reverse-sorted, or partially-sorted arrays.
Beats plain quicksort and merge sort on most real-world data. Used by: Java (Arrays.sort, Collections.sort, Stream.sorted), Python (list.sort, sorted), Android. The standard answer for 'why Java uses TimSort' is: stable + adaptive + O(n log n) worst case + better than quicksort/merge sort on real data.
A common mistake is confusing average and worst case (quicksort O(n log n) avg vs O(n²) worst). Saying merge sort is O(1) space (it's O(n)). Forgetting non-comparison sorts. Not knowing TimSort is what Java uses
Common Mistakes & Pitfalls
Using a.age - b.age (overflow risk). Returning -1/0/1 inconsistently. Forgetting .reversed() is on the comparator, not the class. Mixing up Comparable's compareTo with Comparator's compare
Check your understanding
Answer these without looking back at the article.
- Give an overview of sorting algorithms in Java: bubble, insertion, merge, quicksort, heapsort. Cover time complexity, space complexity, and stability.
- What is TimSort and why does Java use it for `Collections.sort`? Explain its algorithm and performance characteristics.
- How do you implement binary search in Java? Cover iterative and recursive versions, and the correct use of `Collections.binarySearch()`.
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.