← Skill tree CS Skill Tree 0 CSCD211

comparingInt, comparingDouble: Primitive-Friendly Variants

Textbook: BJP (Reges and Stepp)

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?

Tier 1 · BJP (Reges and Stepp), Ch 10


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 because comparingInt is 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?

Tier 2 · BJP (Reges and Stepp), Ch 10


Misconception 2: defaulting to Comparator.comparing for primitive keys

Wrong mental model:Comparator.comparing is 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 int sort key from a type T, and returns a Comparator<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 for Function.


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?

Tier 1 · BJP (Reges and Stepp), Ch 10

What comparison method does Comparator.comparingInt use internally to evaluate two keys?

Tier 1 · BJP (Reges and Stepp), Ch 10

A student writes: Comparator.comparingInt(File::length). What happens at compile time?

Tier 2 · BJP (Reges and Stepp), Ch 10

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?

Tier 2 · BJP (Reges and Stepp), Ch 10

A Team class has String getName() and double getPayroll(). Which comparator correctly sorts by payroll descending, then by name ascending as a tiebreaker?

Tier 3 · BJP (Reges and Stepp), Ch 10