The Fix: Integer.compare and Similar
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. Why the trick fails. (a, b) -> a - b as a Comparator<Integer> is broken. Why?
Check
Integer subtraction overflows silently for extreme values. When a - b exceeds Integer.MAX_VALUE, the result wraps to a negative number, producing the wrong sign and violating antisymmetry.
2. Method signature. What is the full signature of Integer.compare?
Check
public static int compare(int x, int y). It returns a negative integer if x < y, zero if x == y, and a positive integer if x > y.
3. Comparator.comparingInt. What does Comparator.comparingInt(Player::getGamesPlayed) produce?
Check
A Comparator<Player> that extracts an int key from each Player using getGamesPlayed() and compares those keys with Integer.compare.
Try This First
Rewrite (a, b) -> a.getGamesPlayed() - b.getGamesPlayed() without subtraction, in two different ways.
Check
// Explicit: lambda with Integer.compare
(a, b) -> Integer.compare(a.getGamesPlayed(), b.getGamesPlayed())
// Concise: Comparator.comparingInt
Comparator.comparingInt(Player::getGamesPlayed)
Both are overflow-safe. The second is the modern idiomatic form.
What You Need To Walk In With
The Insight: Integer.compare(x, y) uses three-branch comparison logic (x < y ? -1 : (x == y ? 0 : 1)), not subtraction. There is no arithmetic that can overflow. The sign is correct for every possible int pair. This is a drop-in replacement for the subtraction trick with zero trade-offs: same length, same intent, no risk.
Coming in, you need to know how to write a lambda that implements Comparator<T> and what the subtraction trick is and why it fails. From there, the fix is mechanical: identify the numeric type of the field, pick the matching compare method, and if you are comparing on a single field via a getter, consider Comparator.comparingInt as the shortest complete form.
You can: replace any subtraction-based comparator with the appropriate type-specific compare call; choose Long.compare for long fields and Double.compare for double fields; explain why Double.compare is needed even when overflow is not the concern; and write the most concise idiomatic form using Comparator.comparingInt, comparingLong, or comparingDouble.
How It Works
Integer.compare: the safe replacement
// JDK implementation of Integer.compare:
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
No subtraction, no overflow. The sign is always correct.
Direct comparator forms:
// Method reference (shortest)
Comparator<Integer> asc = Integer::compare;
// Lambda (explicit)
Comparator<Integer> asc2 = (a, b) -> Integer.compare(a, b);
Replacing field subtraction in comparators
// BROKEN: subtraction trick
Comparator<Player> byGames = (a, b) -> a.getGamesPlayed() - b.getGamesPlayed();
// FIX 1: Integer.compare in lambda
Comparator<Player> byGames = (a, b) -> Integer.compare(a.getGamesPlayed(), b.getGamesPlayed());
// FIX 2: Comparator.comparingInt (most concise, idiomatic)
Comparator<Player> byGames = Comparator.comparingInt(Player::getGamesPlayed);
All three produce the same ordering. Fix 2 is the preferred modern form.
The full family of safe type-compare methods
| Type | Method | Use for |
|---|---|---|
int |
Integer.compare(int x, int y) |
int fields |
long |
Long.compare(long x, long y) |
long fields (timestamps, IDs) |
double |
Double.compare(double x, double y) |
double fields (salaries, distances) |
float |
Float.compare(float x, float y) |
float fields |
boolean |
Boolean.compare(boolean x, boolean y) |
boolean fields |
Each has the same contract: returns negative, zero, or positive with no overflow risk.
Comparator.comparingInt: the field-extractor shorthand
For object comparators that compare on one int field:
Comparator.comparingInt(Player::getGamesPlayed) // ascending by int field
Comparator.comparingLong(Order::getTimestamp) // ascending by long field
Comparator.comparingDouble(Team::getPayroll) // ascending by double field
These are the cleanest forms when a method reference is available for the key. comparingInt, comparingDouble: Primitive-Friendly Variants covers them in full.
Double.compare: more than just overflow safety
Double.compare handles two edge cases that a - b and a < b ? -1 : 1 do not:
- NaN:
Double.comparedefinesNaNas greater than every other value, including positive infinity. The arithmetic operators (<,>,==) all returnfalsefor any comparison involvingNaN, which makes them unusable for a total order. -0.0vs+0.0:Double.compare(-0.0, +0.0)returns a negative value (-0.0is considered less than+0.0). The==operator considers them equal.
For these reasons, always use Double.compare for floating-point comparators.
Quick check
Check your understanding
A class has a double field getAccuracy(). Which comparator is correct for all possible double values, including NaN?
Worked Example: Predict Then Check
Replace each broken comparator with the cleanest safe alternative:
// A: comparing Integer objects
Comparator<Integer> a = (x, y) -> x - y;
// B: comparing an int field on Player
Comparator<Player> b = (p1, p2) -> p1.getGamesPlayed() - p2.getGamesPlayed();
// C: comparing a long field on Order
Comparator<Order> c = (o1, o2) -> (int)(o1.getTimestamp() - o2.getTimestamp());
Step-by-step reasoning
A: Integer subtraction overflow. Fix: Integer::compare (method reference, shortest).
B: Same overflow risk on int field. Fix: Comparator.comparingInt(Player::getGamesPlayed).
C: Double overflow risk: the long subtraction can overflow; the additional (int) cast truncates and can also lose information. Fix: Comparator.comparingLong(Order::getTimestamp).
Show answers
// A: safe
Comparator<Integer> a = Integer::compare;
// B: safe, idiomatic
Comparator<Player> b = Comparator.comparingInt(Player::getGamesPlayed);
// C: safe, idiomatic
Comparator<Order> c = Comparator.comparingLong(Order::getTimestamp);
Common Misconceptions
Misconception 1: using Integer.compare for long values
Wrong mental model: “
Integer.compareis the safe comparison; I’ll use it for mylongfield by casting.”
Why it breaks: Integer.compare(int x, int y) takes int parameters. Casting a long to int truncates the upper 32 bits. For a long timestamp like 1_700_000_000_000L, the cast to int produces a wrong value. The resulting comparison is incorrect.
How to correct: Use the method that matches the field type: Long.compare(long, long) for long fields, Integer.compare(int, int) for int fields.
Source: Long.compare javadoc.
Quick check
Check your understanding
An Order class has long getTimestamp(). Which call correctly compares two timestamp values without data loss?
Misconception 2: treating a < b ? -1 : 1 as equivalent to Double.compare for floating-point
Wrong mental model: “For doubles, the two-branch ternary
(a, b) -> a < b ? -1 : 1is the same asDouble.comparebecause it avoids subtraction.”
Why it breaks: NaN < x is false for every x, including other NaN values. The two-branch ternary returns 1 for any pair involving NaN, regardless of which argument is NaN. This violates antisymmetry: compare(NaN, 5.0) and compare(5.0, NaN) both return 1. Double.compare handles NaN consistently as greater than all other values.
How to correct: Use Double.compare(a, b) for all floating-point comparators. It is the only standard form that satisfies the total-order contract for all double values.
Source: Double.compare javadoc.
Formal Definition and Interface Contract
From the Java 25 Integer API:
public static int compare(int x, int y)
Compares two
intvalues numerically. The value returned is identical to what would be returned by:Integer.valueOf(x).compareTo(Integer.valueOf(y))
From the Java 25 Double API:
There are two ways in which comparisons performed by this method differ from those performed by the Java language numerical comparison operators (
<,<=,==,>=,>) when applied to primitivedoublevalues:
Double.NaNis considered by this method to be equal to itself and greater than all otherdoublevalues (includingDouble.POSITIVE_INFINITY).0.0dis considered by this method to be greater than-0.0d.
Mental Model
Think of Integer.compare as a referee that asks three questions in order: “Is x less than y? Is x greater than y? Are they equal?” and returns the appropriate result. No arithmetic, no overflow. The subtraction trick asks only “What is x minus y?” and assumes the arithmetic gives the right sign, which fails when the arithmetic overflows. The referee approach never fails.
Connections
Within CSCD 210/211: Static Method Reference as Comparator introduced Integer::compare as a static method reference. This lesson explains why it is not only a style preference but a correctness requirement.
Looking back: The Subtraction Trick and Why It Overflows showed the failure mode. This lesson shows the fix.
Looking ahead: comparingInt, comparingDouble: Primitive-Friendly Variants covers Comparator.comparingInt, comparingLong, and comparingDouble as factory methods that combine key extraction and safe comparison in one call.
Go Deeper (optional)
None of this depth is needed to write correct comparators. The material above is complete. If you are curious where these ideas connect to larger concepts, read on.
Career context. Production codebases compare long timestamps, long database primary keys, and double financial values constantly. A subtraction-based comparator on any of those fields introduces a bug that only surfaces with specific data distributions, which makes it extremely difficult to reproduce in testing. Knowing to reach for Long.compare or Comparator.comparingLong immediately is the kind of judgment that signals you have internalized correctness, not just syntax. Joshua Bloch’s Effective Java, Item 14, documents this pitfall and the fix with the same reasoning as this lesson; Item 43 covers Comparator.comparingInt and the related factory methods as the idiomatic modern form.
Order theory. A Comparator is a binary relation that imposes a total order: the three axioms it must satisfy (antisymmetry, transitivity, totality) are exactly the axioms of a strict total order. The NaN problem with the two-branch ternary is an antisymmetry violation: compare(NaN, x) and compare(x, NaN) return the same sign for every x, which is impossible for a correct total order. Double.compare satisfies all three axioms by defining a consistent convention for NaN that is not derivable from IEEE 754 arithmetic.
Design pattern. A Comparator passed as a parameter is an instance of the Strategy pattern: the ordering algorithm is separated from the object being ordered, so the same class can be sorted many different ways by supplying different comparators. Comparator.comparingInt is a factory method (another pattern) that builds a Strategy from a key-extraction function. The idea that a comparator is an object representing a single responsibility (defining order) and is injected rather than hardcoded traces to David Parnas’s 1972 paper on modularization, which introduced the principle of separating concerns that change independently.
Type theory connection. Passing a Comparator<Player> into Arrays.sort is a form of parametric polymorphism: the sort algorithm is generic, and the ordering behavior is supplied at the call site. The int/long/double distinction in the Comparator.comparingInt family also reflects the has-a relationship between a Player and its int field (composition), as opposed to is-a inheritance, which would hard-code one ordering inside the class itself.
IEEE 754 detail. The reason Double.compare handles NaN and -0.0 consistently is that it compares the IEEE 754 bit patterns as unsigned integers after a sign-magnitude-to-two’s-complement transformation. NaN values have all exponent bits set to 1 and a nonzero mantissa, so their bit patterns are numerically larger than any finite or infinite value. This is why sorting ascending with Double.compare places all NaN values at the end without any explicit check.
Practice
Level 1
Replace each broken comparator:
// (a)
Comparator<Integer> c1 = (a, b) -> a - b;
// (b)
Comparator<Player> c2 = (a, b) -> a.getGamesPlayed() - b.getGamesPlayed();
Show answer
// (a)
Comparator<Integer> c1 = Integer::compare;
// (b)
Comparator<Player> c2 = Comparator.comparingInt(Player::getGamesPlayed);
Level 2
A Transaction class has long getAmount(). The comparator below is broken:
Comparator<Transaction> byAmount = (a, b) -> (int)(a.getAmount() - b.getAmount());
Explain two problems with this code and provide the correct form.
Show answer
Problem 1: long subtraction overflows for large amounts. For example, Long.MAX_VALUE - Long.MIN_VALUE wraps to 0, reporting equal when one amount far exceeds the other.
Problem 2: The (int) cast truncates the long result. Even if the subtraction did not overflow, casting a large long difference to int loses the upper 32 bits, potentially inverting the sign.
Correct form:
Comparator<Transaction> byAmount = Comparator.comparingLong(Transaction::getAmount);
Or:
Comparator<Transaction> byAmount = (a, b) -> Long.compare(a.getAmount(), b.getAmount());
Level 3
A Score class has double getAccuracy(). An array of Score objects may contain elements where accuracy = Double.NaN (from a measurement failure). Write the comparator that sorts Score[] with all NaN values at the end and all finite values sorted ascending by accuracy. Justify your choice of comparison method.
Show answer
Comparator<Score> byAccuracy = Comparator.comparingDouble(Score::getAccuracy);
Arrays.sort(scores, byAccuracy);
Double.compare (used internally by comparingDouble) defines NaN as greater than all other values, including Double.POSITIVE_INFINITY. Sorting ascending places NaN values at the end. No explicit NaN check is needed.
Justification: Double.compare is the only method that satisfies the total-order contract for all double values including NaN. The two-branch ternary (a, b) -> a < b ? -1 : 1 breaks antisymmetry for NaN pairs; subtraction produces NaN when either operand is NaN, which is returned as a double where an int is required (compile error).
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 is the correct overflow-safe replacement for the subtraction trick when writing a Comparator<Integer>?
A Player class has int getGamesPlayed(). Which comparator form is the most concise and idiomatic safe alternative to (a, b) -> a.getGamesPlayed() - b.getGamesPlayed()?
An Order class has long getTimestamp(). A developer writes: Comparator<Order> c = (a, b) -> (int)(a.getTimestamp() - b.getTimestamp()); What are the two problems with this code?
Predict the return value of each call based on the lesson description of Double.compare behavior: A) Double.compare(Double.NaN, 1000.0) B) Double.compare(-0.0, +0.0) C) Double.compare(3.0, Double.NaN)
Why cannot Integer.compare be safely used to compare long field values even though Integer.compare is overflow-safe?