skip to content

How does Comparator.comparing with a key extractor work, and why is it preferred over writing compare by hand?

level: middleimportance: must knowfreq 75%

answer

  1. comparing(keyExtractor) -> compares by the extracted key's natural order
  2. second arg = a Comparator for the key (custom key order)
  3. comparingInt/Long/Double avoid autoboxing + overflow
  4. extractor runs per comparison (O(n log n) times)
  5. returns a Comparator -> chainable with thenComparing/reversed

basics

~20 s

Comparator.comparing takes a function that pulls a sort key out of each object (like person -> person.getAge()), and builds a comparator that orders objects by that key. It's shorter and less error-prone than writing compare yourself.

solid answer

~40 s

Comparator.comparing is a factory: you give it a *key extractor* - a function mapping each element to a Comparable sort key (e.g. Person::getLastName) - and it returns a Comparator that compares two elements by comparing their extracted keys using the key's natural order. You can pass a second argument, a Comparator for the key, to order by a non-natural key order. For primitives there are specialized variants - comparingInt, comparingLong, comparingDouble - that avoid autoboxing and overflow bugs. It's preferred over a hand-written compare because it's declarative, composable (you can chain thenComparing, reversed, nullsFirst), reads like the intent ('compare by last name'), and reuses correct, contract-respecting comparisons of the key type instead of risking subtraction overflow or sign mistakes.

go deeper

for a junior

Can use Comparator.comparing with a method reference to sort by one field.

for a middle

Chooses comparingInt/Long/Double for primitive keys, passes a key comparator for custom key order, and knows the result is chainable.

for a senior

Explains that the extractor runs per comparison, the boxing/overflow advantages of the primitive variants, and when to precompute keys.

for a principal

Weighs decorate-sort-undecorate vs comparing on hot paths, and designs comparator factories for reuse across a codebase.

## Building comparators declaratively Writing `compare` by hand is repetitive and bug-prone. Java 8 added factory methods that build comparators from a **key extractor** - a function that, given an element, returns the value to sort by. ### The basic form ```java Comparator<Person> byLastName = Comparator.comparing(Person::getLastName); people.sort(byLastName); ``` Here `Person::getLastName` is the key extractor (a `Function<Person, String>`). `comparing` returns a `Comparator<Person>` whose `compare(p1, p2)` effectively does: ```java return p1.getLastName().compareTo(p2.getLastName()); ``` It extracts the key from each element and compares the **keys** using the key type's natural ordering (here `String`'s). The extracted key must itself be `Comparable`. ### Supplying a custom key ordering If the key shouldn't use natural order, pass a second argument - a `Comparator` for the key: ```java Comparator<Person> byLastNameCaseInsensitive = Comparator.comparing(Person::getLastName, String.CASE_INSENSITIVE_ORDER); ``` Now the keys are compared with the given key-comparator instead of `compareTo`. ### Primitive specializations: comparingInt/Long/Double ```java Comparator<Person> byAge = Comparator.comparingInt(Person::getAge); ``` If you wrote `Comparator.comparing(Person::getAge)` with an `int` getter, the int is **autoboxed** to `Integer` for every comparison. `comparingInt` takes a `ToIntFunction` and compares the raw ints, avoiding boxing (less garbage, faster) and using `Integer.compare` internally (no subtraction overflow). Prefer the primitive variant whenever the key is a primitive. ### Why prefer it over hand-written compare 1. **Declarative & readable** - `comparing(Person::getAge)` says *what*, not *how*. 2. **Composable** - the result is a `Comparator`, so you can immediately chain `.thenComparing(...)`, `.reversed()`, `Comparator.nullsFirst(...)`. 3. **Correct by construction** - it delegates to the key type's own `compareTo`/given comparator, dodging classic mistakes like `a - b` overflow or returning the wrong sign. 4. **Less code** - one line vs a multi-branch method. ### Performance note The key extractor runs **for each comparison**, i.e. roughly O(n log n) times during a sort, not once per element. If extracting the key is expensive, that cost multiplies. For very hot paths consider a *decorate-sort-undecorate* (Schwartzian transform) - precompute keys once - though for most code `comparing` is fine. ### Putting it together ```java people.sort( Comparator.comparing(Person::getLastName) .thenComparingInt(Person::getAge) .reversed()); ``` Reads as: by last name, tie-break by age, then reverse the whole thing.

  • Why use comparingInt over comparing for an int getter?
    comparing autoboxes each int to Integer on every comparison; comparingInt works on raw ints, avoiding boxing overhead and using Integer.compare so there's no overflow.
  • How many times is the key extractor invoked during a sort of n elements?
    Roughly O(n log n) times - once for each side of each pairwise comparison, not once per element.

saying these in an interview costs you the question

  • Using comparing for a primitive key instead of comparingInt (needless boxing)
  • Assuming the key extractor runs once per element (it runs per comparison)
  • Thinking comparing mutates the list (it returns a Comparator)
  • Passing a non-Comparable key to the single-arg comparing (won't compile/throws)

context