comparingInt, comparingDouble: Primitive-Friendly Variants
Before You Start
Check each box you can do from memory. A box you cannot check yet is not a problem; it points you to a quick refresher, not a grade.
Not sure? Take the 60-second self-check.
1. Autoboxing cost. What happens when int getGamesPlayed() is passed to a parameter expecting Integer?
Check
Java autoboxes the int to an Integer object. Each autobox allocation is a new object on the heap, which adds garbage-collection pressure.
2. Safe comparison. What does Integer.compare(a, b) return when a < b?
Check
A negative integer. It never overflows, because it uses a three-branch comparison, not subtraction.
3. Variant naming. The method comparingInt applies to keys returned by what kind of function?
Check
A ToIntFunction<T>: a function from T to int. It extracts a primitive int key with no boxing.
Try This First
Compare these two comparators:
// A
Comparator<Player> a = Comparator.comparing(Player::getGamesPlayed);
// B
Comparator<Player> b = Comparator.comparingInt(Player::getGamesPlayed);
Before reading: which is preferred for a Player[] of 100,000 elements? What is the difference?
Check
b is preferred. a autoboxes each int key to Integer; over 200,000 key extractions in a sort, that is 200,000 Integer allocations. b uses ToIntFunction<Player> and Integer.compare internally, producing zero boxing. Both produce the same sort order.
What You Need To Walk In With
The Insight: The primitive-friendly variants (comparingInt, comparingLong, comparingDouble) are not advanced features; they are the default choice when the key is a primitive type. Using Comparator.comparing for a primitive key autoboxes silently. The primitive variant is equally concise and avoids the allocation overhead with no other trade-off.
To use them fluently, you need a clear picture of what your accessor actually returns: when the method returns int, the right tool is comparingInt; when it returns long, the right tool is comparingLong; when it returns double, the right tool is comparingDouble. You also need to know that Integer.compare is what powers these variants internally, so the safety guarantee from the safe-comparison material carries forward here automatically. You can: choose the correct primitive variant by reading the accessor’s declared return type, chain it with reversed() to flip the order, add thenComparingInt (or the sibling methods) as a tiebreaker, and convert a named-class comparator that calls Integer.compare directly into a single comparingInt expression.
How It Works
The three variants and their function types
| Method | Parameter type | Key return type | Internal comparison |
|---|---|---|---|
Comparator.comparingInt(ToIntFunction<T>) |
T -> int |
int |
Integer.compare(int, int) |
Comparator.comparingLong(ToLongFunction<T>) |
T -> long |
long |
Long.compare(long, long) |
Comparator.comparingDouble(ToDoubleFunction<T>) |
T -> double |
double |
Double.compare(double, double) |
All three are overflow-safe because the underlying comparison method is the type-specific compare, not subtraction. This makes Comparator.comparingInt(Player::getGamesPlayed) not only boxing-free but also equivalent to the safe (a, b) -> Integer.compare(a.getGamesPlayed(), b.getGamesPlayed()).
Choosing based on return type
// int field
Comparator<Player> byGames = Comparator.comparingInt(Player::getGamesPlayed);
// long field (timestamps, large IDs)
Comparator<Order> byTime = Comparator.comparingLong(Order::getTimestamp);
// double field (salaries, distances)
Comparator<Team> byPayroll = Comparator.comparingDouble(Team::getPayroll);
Chaining primitive variants
The primitive variants have corresponding thenComparing* siblings for multi-key sorts:
// Primary: team (String key, use comparing)
// Secondary: games played (int key, use thenComparingInt)
Comparator<Player> byTeamThenGames =
Comparator.comparing(Player::getTeam)
.thenComparingInt(Player::getGamesPlayed);
thenComparingInt, thenComparingLong, and thenComparingDouble accept primitive-returning functions as tiebreakers.
In 211 lab work
Every comparator in archived labs that orders by an int field (gamesPlayed, jerseyNumber, pageCount) should use comparingInt. Every double field (payroll, price, salary) should use comparingDouble. The named-class forms in the labs often use Integer.compare in the body; comparingInt produces the same result in one expression.
Quick check
Check your understanding
A Product class has double getPrice() returning a primitive double. Which factory method produces the correct boxing-free comparator?
Worked Example: Predict Then Check
Build a Comparator<Team> that sorts by payroll (a double) descending, then by team name (String) ascending as a tiebreaker.
Reasoning
- Primary: payroll (double), descending. Use
comparingDouble(...).reversed(). - Secondary: team name (String), ascending. Use
thenComparing(Team::getName).
Show answer
Comparator<Team> c = Comparator.comparingDouble(Team::getPayroll)
.reversed()
.thenComparing(Team::getName);
comparingDouble extracts the double payroll and uses Double.compare (no boxing, handles NaN). reversed() flips to descending. thenComparing(Team::getName) breaks ties alphabetically by name.
Common Misconceptions
Misconception 1: choosing the wrong primitive variant
Wrong mental model: “
Comparator.comparingInt(File::length)is correct becausecomparingIntis for any numeric field.”
Why it breaks: File.length() returns long, not int. comparingInt expects ToIntFunction<T>, which requires an int-returning method. Passing a long-returning method reference produces a compile error: “incompatible types: bad return type in lambda expression.”
How to correct: Use Comparator.comparingLong(File::length). Match the variant to the accessor’s declared return type: int maps to comparingInt, long to comparingLong, double to comparingDouble.
Quick check
Check your understanding
A student writes Comparator.comparingInt(File::length) intending to sort files by size. What is wrong?
Misconception 2: defaulting to Comparator.comparing for primitive keys
Wrong mental model: “
Comparator.comparingis the standard method; use the primitive variants only when performance matters.”
Why it matters: Autoboxing is silent. Comparator.comparing(Player::getGamesPlayed) compiles, runs correctly, and produces the right order. But for large arrays or frequently sorted collections, the per-extraction Integer allocation accumulates. Modern lints (SpotBugs, ErrorProne) flag this as a performance issue. The primitive variant is equally readable.
How to correct: Default to comparingInt for int keys, comparingLong for long, comparingDouble for double. Use the object form only for String, LocalDate, or other Comparable types.
Source: Bloch, Effective Java, Item 6.
Formal Definition and Interface Contract
From the Java 25 Comparator.comparingInt API:
static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)
Accepts a function that extracts an
intsort key from a typeT, and returns aComparator<T>that compares by that sort key. The returned comparator is serializable if the specified function is also serializable.The extracted key values are compared using
Integer.compare(int, int).
From the Java 25 ToIntFunction API:
@FunctionalInterface
public interface ToIntFunction<T>
Represents a function that produces an int-valued result. This is the
int-producing primitive specialization forFunction.
Mental Model
Think of the three variants as the same factory Comparator.comparing but with a different output pipe. The generic comparing runs the extracted key through the Comparable interface (which involves boxing for primitives). The comparingInt variant connects the output directly to Integer.compare with no box in between. Same logical operation, different plumbing for primitive keys.
Connections
Within CSCD 210/211: The safe-comparison material showed that Integer.compare is the overflow-safe comparison for int (The Fix: Integer.compare and Similar). This lesson shows Comparator.comparingInt as the API that uses Integer.compare internally, making it the preferred one-expression form for int-key comparators.
Looking back: The Comparator.comparing by Key lesson covered Comparator.comparing for object keys. This lesson is the parallel form for primitive keys.
Looking ahead: This is the last comparator topic. The composition and enums material comes next, where the Comparable<T> contract that runs through the comparator material reappears in the context of enum types.
Practice
Level 1
Choose the correct variant and write the comparator:
(a) Comparator<Player> by getGamesPlayed() (returns int) ascending.
(b) Comparator<Order> by getTimestamp() (returns long) ascending.
(c) Comparator<Team> by getPayroll() (returns double) ascending.
Show answer
// (a)
Comparator<Player> a = Comparator.comparingInt(Player::getGamesPlayed);
// (b)
Comparator<Order> b = Comparator.comparingLong(Order::getTimestamp);
// (c)
Comparator<Team> c = Comparator.comparingDouble(Team::getPayroll);
Level 2
A student writes Comparator.comparingInt(File::length). Explain the compile error and provide the correct form.
Show answer
File.length() returns long. comparingInt expects a ToIntFunction<File> (a function returning int). Passing a long-returning method reference is a type mismatch.
Correct form:
Comparator.comparingLong(File::length)
Level 3
A Package class has String getType() and double getWeightOz(). Write a comparator that sorts packages by type ascending, then by weight descending as a tiebreaker. Use the most concise primitive-friendly forms.
Show answer
Comparator<Package> byTypeAndWeight =
Comparator.comparing(Package::getType)
.thenComparingDouble(Package::getWeightOz)
.reversed();
Wait: .reversed() would reverse the whole chain, making type descending too. Instead, reverse only the weight part:
Comparator<Package> byTypeAndWeight =
Comparator.comparing(Package::getType)
.thenComparing(
Comparator.comparingDouble(Package::getWeightOz).reversed());
Reading: compare by type ascending; for packages with equal type, compare by weight descending. The Comparator.comparingDouble(...).reversed() sub-expression produces the descending-weight comparator, which thenComparing uses as the tiebreaker.
Go Deeper (optional)
None of what follows is needed to write correct, professional code. These connections are here for readers who find themselves wondering where the design came from, or who encounter this material again in a systems or programming-languages context.
Where the primitive specialization comes from. Java generics cannot hold primitives: a List<int> is impossible because generics use type erasure and primitives have no object representation on the heap. The JDK’s response was to build a parallel hierarchy of primitive-specialized functional interfaces: ToIntFunction<T>, IntUnaryOperator, IntBinaryOperator, and so on. comparingInt is the Comparator factory that bridges the generic world to this primitive world. When you reach for comparingInt, you are touching a deliberate type-system workaround built into the standard library to avoid the cost that erasure would otherwise impose. Bloch, Effective Java, Item 43, covers this philosophy in full.
Production systems care about this. Collections sorted on hot paths, such as user feeds ordered by timestamp, product catalogs ordered by price, or leaderboards ordered by score, call the comparator millions of times. The allocation budget on a GC-sensitive service is real: each autoboxed Integer is a short-lived heap object that adds pressure to the garbage collector. Tools like SpotBugs and Google ErrorProne include rules that flag Comparator.comparing used on a primitive-returning method, precisely because the primitive variant is equally concise and costs nothing extra. Defaulting to comparingInt for int keys is the professional norm, and code reviewers notice when it is missing.
The order-theory angle. A Comparator<T> imposes a total order on T: it is a binary relation that is transitive, complete (every pair is comparable), and antisymmetric (consistent with equals when the comparator is consistent). The three-variant design preserves those axioms regardless of which specialization you choose; Integer.compare guarantees the same sign contract as any correct comparator. If you study order theory or discrete math later, you will recognize Comparator as a concrete implementation of a strict total order, and the axioms in the Javadoc are exactly the axioms of that relation.
Check Yourself
Close the notes and answer each one from memory, then reveal it. Pulling an idea back from memory is one of the strongest ways to make it stick.
Check your understanding
Which method should you use to build a Comparator<Player> that sorts by getGamesPlayed(), which returns int?
What comparison method does Comparator.comparingInt use internally to evaluate two keys?
A student writes: Comparator.comparingInt(File::length). What happens at compile time?
Predict the sort order produced by this code when applied to a Player[] where gamesPlayed values are 30, 10, 20: Comparator<Player> c = Comparator.comparingInt(Player::getGamesPlayed).reversed(); Arrays.sort(players, c); // What order are the gamesPlayed values after the sort?
A Team class has String getName() and double getPayroll(). Which comparator correctly sorts by payroll descending, then by name ascending as a tiebreaker?