← Skill tree CS Skill Tree 0 CSCD211

compare Returns a Sign, Not a Magnitude

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. What does Comparator<T> look like? Write the method signature for compare.
Check

int compare(T a, T b), it takes two objects of the same type and returns an int. The method is defined in java.util.Comparator. (BJP (Reges and Stepp))

  1. What does a negative return value mean? If compare(x, y) returns a negative number, what does the sort do with x relative to y?
Check

A negative return tells the sort to place x before y in the result. The sort does not read the exact value, only whether it is negative, zero, or positive.

  1. What are the three possible outcomes a compare implementation must cover?
Check
  1. a belongs before b: return any negative integer.
  2. a and b are equal in the ordering: return 0.
  3. a belongs after b: return any positive integer.

If you missed the zero case, review antisymmetry and transitivity before continuing.


Try This First

Here are two Comparator<Integer> implementations. Before reading further, predict whether they produce the same sorted array or different ones for [5, 1, 9, 3].

// Comparator A
Comparator<Integer> byValueA = (a, b) -> a.compareTo(b); // returns -1, 0, or 1

// Comparator B
Comparator<Integer> byValueB = (a, b) -> a - b;           // returns actual difference

Write down your prediction: same result, or different?

What actually happens

Both produce [1, 3, 5, 9], identical arrays. For inputs with no overflow risk, a - b and a.compareTo(b) carry the same sign even though their magnitudes differ. Arrays.sort reads only the sign and throws away the number itself.


What You Need To Walk In With

compare and compareTo report only direction: negative, zero, or positive. The exact magnitude of the returned integer is never read by Arrays.sort, TreeSet, PriorityQueue, or any other Java collection. Testing the return value for == -1 is a bug, because the contract permits any negative int. A correct compare body answers one question: “which element comes first?” You signal that answer with the sign of the return value, and the rest is noise. You can: predict the sort outcome for any return value knowing only its sign, identify a brittle == -1 check on sight, write a correct comparator body using Integer.compare, and explain in one sentence why magnitude carries no information for a sorting algorithm.


How It Works

The three buckets

The compare contract partitions every pair (a, b) into exactly three buckets:

Return value Meaning What the sort does
Any negative int a belongs before b moves a earlier
0 a and b are equal in this ordering leaves relative position unchanged
Any positive int a belongs after b moves a later

The number -1000000 lands in the same bucket as -1. The number 42 lands in the same bucket as 1. The bucket is what counts; the magnitude is discarded.

A picture

compare(a, b) returns:

  ... -100  -7  -1   0   1   7   100 ...
  |__________________________|_______|_______|
          "a before b"  "equal"  "a after b"
          (any negative)  (zero)  (any positive)

Every value in the left region triggers the same action. Every value in the right region triggers the same action. The sort never subtracts two return values or accumulates them.

Canonical code skeleton

The cleanest pattern uses the static helper methods from the wrapper types:

// Preferred: Integer.compare documents intent and is overflow-safe
Comparator<Person> byAge = (a, b) -> Integer.compare(a.getAge(), b.getAge());

// Preferred: for String fields, delegate to String's own compareTo
Comparator<Person> byName = (a, b) -> a.getName().compareTo(b.getName());

// Acceptable: explicit ternary, covers all three cases
Comparator<Person> byAge2 = (a, b) ->
    a.getAge() < b.getAge() ? -1 : (a.getAge() > b.getAge() ? 1 : 0);

The subtraction shortcut a.getAge() - b.getAge() returns the right sign when there is no overflow, but the subtraction trick and why it overflows shows when it silently breaks. Integer.compare is always safe.

Why the sort only needs a sign

Arrays.sort implements a total order: for any pair of elements, it needs to know which one is “smaller”, whether they are “equal”, or which is “larger”. That decision requires exactly three buckets. A 32-bit integer gives you over four billion possible values, but only three of them matter: negative, zero, positive. All other information is thrown away.

Source: java.util.Comparator, compare(T,T); Bloch, Effective Java, Item 14.

Quick check

Check your understanding

compare(a, b) returns -42. What does Arrays.sort do with that result?

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


Worked Example: Predict Then Check

Setup

Comparator<String> byLength = (a, b) -> a.length() - b.length();
String[] words = {"fig", "kiwi", "plum", "date"};
Arrays.sort(words, byLength);

Predict: What is words[0] after the sort?

Work through one comparison mentally:

Check your prediction

words[0] is "fig" (length 3, the shortest). The exact differences (-1, 0) do not matter; only the signs guided the sort. The result is ["fig", "date", "kiwi", "plum"] or ["fig", "date", "plum", "kiwi"] (the two four-letter words may appear in either order because the comparator treats them as equal).

Algorithmic steps for writing a correct compare

  1. Identify the field or fields you are ordering by.
  2. For numeric fields: use Integer.compare(a.getField(), b.getField()) or the appropriate wrapper’s static compare.
  3. For String, LocalDate, or any Comparable field: use a.getField().compareTo(b.getField()).
  4. For multi-field ordering: chain with if (primary != 0) return primary; then compute the secondary.
  5. Never return just a.getField() < b.getField() ? -1 : 1. That omits the zero case.

Common Misconceptions

Common misconception

Magnitude affects sort order.

Wrong mental model: “Returning -100 will make the sort push a much further left than returning -1 would.”

Why it breaks on a small example: sort [2, 1] with a comparator returning -99 for (2, 1) versus one returning -1. Both produce [1, 2]. The sort only checked the sign.

How to correct: Return any negative for “before”, any positive for “after”, zero for “equal”. -1, 0, 1 are conventional and communicate intent clearly.

Source: java.util.Comparator, compare(T,T); Bloch, Effective Java, Item 14.

Quick check

Check your understanding

Comparator X returns -1 for a pair. Comparator Y returns -99 for the same pair. How do Arrays.sort’s results differ?

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


Common misconception

A boolean cast covers all the cases.

Wrong mental model: “Less-than is all I need; I will write return a.gamesPlayed < b.gamesPlayed ? -1 : 1;.”

Why it breaks on a small example: compare(x, x) where x.gamesPlayed == x.gamesPlayed returns 1, not 0. TreeSet then treats x and x as distinct elements, so the same object can be inserted twice.

How to correct: Always include the equal case. Use Integer.compare(a.gamesPlayed, b.gamesPlayed) or an explicit three-way ternary.

Source: Bloch, Effective Java, Item 14; JLS 25 §15.20.1 (relational operators produce boolean, not a three-valued result).


Formal Definition and Interface Contract

From the Java 25 API (java.util.Comparator.compare):

Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.

The specification says “a negative integer” and “a positive integer”, not “exactly -1” and “exactly 1”. The contract deliberately leaves magnitude unspecified. Any code that depends on a specific magnitude (for example, a downstream if (result == -1)) is going beyond the contract and will break with any compliant comparator that returns -42.

Bloch, Effective Java, Item 14 (“Consider implementing Comparable”) states the same principle for compareTo, which shares the sign-only contract with compare.


Mental Model

Think of compare as a three-position toggle switch: flip left for “a first”, flip right for “b first”, leave centered for “same”. The force you apply to the switch does not matter, only which way it goes. Every sorting algorithm in the Java Collections Framework reads only the position of that toggle, never the force. Writing -1, -7, or -Integer.MAX_VALUE all flip the same switch in the same direction.


Connections

Within CSCD 211: This concept is the foundation for the rest of the compare contract. Antisymmetry and transitivity, and consistency with equals all assume you already understand that the sign is the only output that matters.

Looking back: In CSCD 210 you compared objects with equals for identity. compare extends that to ordering: instead of true/false you get three outcomes. The boolean case is a degenerate version of the sign-only case.

Looking ahead: The subtraction trick and why it overflows shows the one situation where returning a - b instead of Integer.compare(a, b) silently produces the wrong sign. That failure mode will be immediately clear after mastering the sign-only contract here.


Go Deeper (optional)

None of what follows is needed to write correct comparators. If you are solidly past the quiz, though, these connections show where the sign-only rule comes from and why it keeps appearing in other languages and fields.

Where the rule comes from in mathematics. A Comparator is a formal encoding of a total order: a binary relation on a set that is transitive, antisymmetric, and total (every pair is comparable). The trichotomy law, which says exactly one of a < b, a = b, or a > b holds for any pair, is the same law that makes the three-bucket partition exhaustive. Magnitude would add no information to that structure because a total order cares only about direction, not distance. That is why the sign-only contract is not an arbitrary Java design choice: it is the minimal faithful representation of what a total order actually expresses.

The design-pattern angle. A Comparator is the simplest instance of the Strategy pattern: the ordering rule is separated from the objects being ordered, and it is passed around as a first-class object. Java 8 made that explicit by making Comparator<T> a functional interface, so a lambda expression is literally a function value being treated as data. The sign-only contract is also the simplest example of a function that returns a value whose type (trichotomy) is richer than boolean but much simpler than a full integer. That same idea appears in C++20’s spaceship operator <=> and in Python 2’s cmp() function, which share identical three-bucket semantics.

Has-a versus is-a. Giving a class its own compareTo (by implementing Comparable) says “this type has a natural order baked in.” Injecting a Comparator from outside says “I am adding an ordering that the class does not own.” That is has-a versus is-a applied to behavior rather than state, and it maps to the same composition-over-inheritance principle David Parnas articulated in 1972 when he argued that a module’s design should follow from the decisions it encapsulates.

Career relevance. Every Java collection that stores sorted data (TreeMap, TreeSet, PriorityQueue) enforces this contract at runtime. A comparator that tests == -1 is a latent correctness bug that will silently corrupt a TreeSet in production, because any compliant comparator implementation is free to return -42. This is the kind of contract-violation bug that appears in Java technical interviews precisely because it is easy to introduce and hard to notice in testing.


Practice

Level 1

A student writes:

Comparator<Integer> c = (a, b) -> a - b;
int result = c.compare(3, 10);
System.out.println(result == -1); // does this print true?

Predict: does the println print true or false?

Thought process

3 - 10 = -7. Is -7 == -1? No. The contract says “any negative integer”, so -7 is a valid and correct return value, but it is not -1.

Show answer

The println prints false. c.compare(3, 10) returns -7, not -1. Code that checks == -1 (or == -1 after a sort comparison) is outside the contract and will fail with many legal comparator implementations. Use result < 0 instead of result == -1 when you need to test the direction.


Level 2

Write Comparator<Person> byNameLength so that shorter names sort first. The body must be one return statement and must use Integer.compare.

record Person(String name, int age) {}

Comparator<Person> byNameLength = (a, b) -> /* your code here */;
Thought process

You want shorter names first, so if a.name() is shorter than b.name(), compare should return a negative number. Translate: “compare the lengths of the names”. Use Integer.compare with a.name().length() as the first argument and b.name().length() as the second.

Show answer
Comparator<Person> byNameLength = (a, b) ->
    Integer.compare(a.name().length(), b.name().length());

Integer.compare(x, y) returns a negative int when x < y, zero when x == y, and a positive int when x > y. Shorter names produce smaller lengths, so shorter names come first, exactly the ordering requested.


Level 3

A teammate proposes this comparator for a leaderboard where higher scores rank first:

Comparator<Player> byScore = (a, b) -> {
    if (a.score() > b.score()) return 1;
    if (a.score() < b.score()) return -1;
    return 0;
};

The teammate says: “Higher scores should come first, so I return 1 when a has a higher score.” Identify the bug and fix it with one line.

Thought process

Think about what “a before b” means in the context of a leaderboard. If higher scores rank first, then a player with score 90 should appear before a player with score 70. In a compare call with (a=90-player, b=70-player), you want a before b, so compare must return a negative value. But the code returns 1 (positive) when a.score() > b.score(). That puts the higher-scoring player after the lower-scoring one.

Show answer

The return values are reversed. compare(a, b) must return negative when a should come first. For a descending score order, a comes first when a.score() > b.score(), so that case needs a negative return.

Fix (one line):

Comparator<Player> byScore = (a, b) -> Integer.compare(b.score(), a.score());

Swapping the arguments to Integer.compare reverses the order: b.score() as the first argument means “if b’s score is larger, return positive, i.e., put b before a”, which is descending order. Alternatively, negate the result: return -Integer.compare(a.score(), b.score()).

The sign is the only output that matters; both fixes produce the correct sign for every pair.


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 of the following is a valid return value from a correct compare(a, b) implementation when a belongs before b?

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

A student writes: if (comparator.compare(x, y) == -1) { ... }. What is wrong with this check?

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

Trace this code: Comparator<Integer> c = (a, b) -> a - b; System.out.println(c.compare(5, 12)); What prints?

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

A Comparator<Person> is written as: (a, b) -> a.score() > b.score() ? -1 : 1. What input exposes the bug in this implementation?

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

Which of these compare implementations is correct AND overflow-safe for sorting Person objects by age in ascending order?

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