Writing a Comparator as a Lambda
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. Anonymous-class form: What is the name of the one abstract method that every Comparator<T> must implement?
Check
compare. Its signature is int compare(T o1, T o2). Returning a negative number means o1 < o2, zero means equal, positive means o1 > o2. (Source: Comparator javadoc)
2. Interface contract: If you write Arrays.sort(arr, cmp), what does Java require of cmp?
Check
cmp must implement Comparator<T> where T matches the element type of arr. Concretely, it must supply a compare(T a, T b) method that returns an int. If you are fuzzy on this, review the anonymous-class comparator first.
3. Functional interface: How many abstract methods does a functional interface have?
Check
Exactly one. That is what makes a lambda able to stand in for it: the compiler knows which method the lambda body is implementing. Comparator<T> qualifies because compare is its only abstract method. (Source: JLS §15.27.3)
Try This First
You have an array of Player objects and you want to sort them by games played, ascending. Here is the anonymous-class version:
Arrays.sort(players, new Comparator<Player>() {
@Override
public int compare(Player a, Player b) {
return Integer.compare(a.getGamesPlayed(), b.getGamesPlayed());
}
});
Before reading the next section, count the characters that carry the actual logic versus the characters that are boilerplate. Then predict: what is the minimum you would need to keep if you could strip everything else away?
What You Need To Walk In With
A lambda is the same one-method function object as the anonymous class, written inline because Comparator is a functional interface. The compiler fills in the type, the class declaration, the method name, and the @Override annotation automatically. If writing any of this feels like a lot at once, focus on Form 1 alone: two parameter names, an arrow, and one expression. That covers the vast majority of real sorting code, and the other forms follow naturally once Form 1 is solid.
You can: write a Comparator<T> lambda in expression form for a single-expression body, write the block form when the body needs more than one statement, identify the inferred parameter types, and translate an anonymous-class Comparator into its lambda equivalent.
How It Works
Lambda anatomy
A lambda has three parts:
( parameters ) -> body
For a Comparator<Player> there are always two parameters (the two objects being compared) and the body must produce an int.
The three common forms
// Form 1: expression body, inferred types (preferred when the body is one expression)
(a, b) -> Integer.compare(a.getGamesPlayed(), b.getGamesPlayed())
// Form 2: expression body, explicit types (use when inference is ambiguous)
(Player a, Player b) -> a.getTeam().compareTo(b.getTeam())
// Form 3: block body (required when you need more than one statement)
(a, b) -> {
int byTeam = a.getTeam().compareTo(b.getTeam());
if (byTeam != 0) return byTeam;
return Integer.compare(a.getGamesPlayed(), b.getGamesPlayed());
}
How the compiler fills in the blanks
When you write:
Arrays.sort(players, (a, b) -> Integer.compare(a.getGamesPlayed(), b.getGamesPlayed()));
Java sees that Arrays.sort(Player[], Comparator<? super Player>) expects a Comparator<Player> in the second slot. The compiler therefore:
- Concludes the lambda implements
Comparator<Player>(JLS §15.27.3). - Assigns type
Playerto bothaandb(JLS §15.27.1). - Wires the body to the
comparemethod of that interface.
Nothing else is needed. The class wrapper, the method name, and the return type are all derived from context.
Picture: anonymous class versus lambda
ANONYMOUS CLASS LAMBDA
─────────────────────────────────── ──────────────────────────────
new Comparator<Player>() { (a, b) ->
@Override Integer.compare(
public int compare(Player a, a.getGamesPlayed(),
Player b) { b.getGamesPlayed())
return Integer.compare(
a.getGamesPlayed(),
b.getGamesPlayed());
}
}
Everything to the left of the arrow in the lambda column was generated by the compiler. Only the parameter list and the body are yours to write.
Worked Example: Translate an Anonymous Class to a Lambda
The starting point
Arrays.sort(books, new Comparator<Book>() {
@Override
public int compare(Book a, Book b) {
return a.getISBN().compareTo(b.getISBN());
}
});
(Source: modernization of Bloch EJ3 Item-42)
Predict: What does the lambda version look like?
Step-by-step translation
Step 1: Identify the parameter list. The anonymous class declares compare(Book a, Book b). The lambda parameter list is (a, b). Types are inferred from the Comparator<Book> context.
Step 2: Identify the body. The body is a single return statement. Drop the braces, the return keyword, and the semicolon. What remains is the expression:
a.getISBN().compareTo(b.getISBN())
Step 3: Assemble.
Arrays.sort(books, (a, b) -> a.getISBN().compareTo(b.getISBN()));
Step 4: Verify the expected output matches.
Expected (from objective.md Stem 2): (a, b) -> a.getISBN().compareTo(b.getISBN()). Matches.
Show answer
Arrays.sort(books, (a, b) -> a.getISBN().compareTo(b.getISBN()));
The compiler infers a and b are Book from the surrounding Comparator<Book> context. The expression body a.getISBN().compareTo(b.getISBN()) evaluates to an int, satisfying compare’s return type.
Algorithmic steps for any translation
- Strip
new Comparator<T>() { @Override public int compare(and the closing}}. - Keep the parameter names; drop the explicit types unless inference is ambiguous.
- If the body is a single
return expr;, replace with-> expr(no braces, noreturn). - If the body has two or more statements, keep braces and keep each
return.
Quick check
Check your understanding
You translate an anonymous-class Comparator<Book> whose body is a single return statement into a lambda. Which of the following is the correct expression-body form?
Common Misconceptions
Block body works like an expression body.
Wrong mental model: Wrapping the body in { } is just style; the last expression is still the return value.
Why it breaks: Try compiling (a, b) -> { a.compareTo(b) }. The compiler reports a type error because a block body requires an explicit return statement to produce a value. Block bodies are sequences of statements, not expressions.
Correction: Use either (a, b) -> a.compareTo(b) (expression, no braces, no return) or (a, b) -> { return a.compareTo(b); } (block, braces, explicit return). The two forms are not interchangeable.
Source: JLS §15.27.2; common student error.
You can annotate one parameter type but not the other.
Wrong mental model: (Player a, b) -> ... is valid because only the first parameter needs a hint.
Why it breaks: The line (Player a, b) -> a.compareTo(b) does not compile. Java requires either both parameters typed or neither. Mixing is a syntax error (JLS §15.27.1).
Correction: Either omit both types ((a, b) -> ...) or supply both ((Player a, Player b) -> ...). Omitting both is preferred when the target type makes inference unambiguous.
Source: JLS §15.27.1; observed in F19 grading.
An expression-body lambda needs a return keyword.
Wrong mental model: (a, b) -> return a.compareTo(b) mirrors a regular method body.
Why it breaks: return is a statement; it is only legal inside a block body { }. In expression form the value of the expression itself is returned. Writing return there is a compile error (JLS §15.27.2).
Correction: Drop return: (a, b) -> a.compareTo(b). If you want return, add braces: (a, b) -> { return a.compareTo(b); }.
Source: JLS §15.27.2.
Quick check
Check your understanding
A student writes this lambda and gets a compile error: (a, b) -> { a.getTeam().compareTo(b.getTeam()) }. What is the cause?
Formal Definition and Interface Contract
Comparator<T> is a functional interface (JLS §9.8) with a single abstract method:
int compare(T o1, T o2)
Because it has exactly one abstract method, a lambda expression can satisfy it wherever a Comparator<T> is expected (JLS §15.27.3).
The full specification lives at: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Comparator.html
Bloch EJ3 Item-42 states: prefer lambdas to anonymous classes. Anonymous classes are now obsolete for functional interfaces. This is the authoritative style guidance.
The numerical contract is unchanged from the anonymous-class form:
- Negative return: first argument is less than second.
- Zero: arguments are equal for sorting purposes.
- Positive: first argument is greater than second.
Mental Model
Think of a Comparator lambda as a miniature function that takes two objects and answers the question “which one comes first?” The anonymous class was a full filing cabinet to hold that single function; the lambda is just the function, handed directly to Arrays.sort without wrapping. The compiler builds the filing cabinet in the background. What you write is only what changes from one comparator to the next: the two parameter names and the comparison expression.
Connections
Within CSCD 210/211/300: The lambda form replaces the anonymous-class Comparator and is itself replaced by method references for the common case where the body is a single method call.
Looking back: This builds on the interface mechanism from CSCD 210 (an interface defines a contract) and the anonymous-class Comparator form, which the lambda directly compresses.
Looking ahead: Method references compress the lambda further when the body is a direct delegation to an existing method. Comparator chaining with thenComparing composes multiple lambdas into a multi-key sort.
Practice
Level 1
Problem: Sort Player[] players ascending by games played using a single-line lambda passed directly to Arrays.sort.
Thought process
You need a Comparator<Player> that delegates to Integer.compare for numeric fields. Integer.compare(x, y) returns negative when x < y, which is ascending order. Write it as an expression-body lambda with inferred parameter types.
Show answer
Arrays.sort(players, (a, b) -> Integer.compare(a.getGamesPlayed(), b.getGamesPlayed()));
a and b are inferred as Player from the Comparator<? super Player> parameter of Arrays.sort. (Source: objective.md Stem 1; modernization of F19.Lab1.CSCD211Lab1.java case 6)
Level 2
Problem: Rewrite the following as a lambda:
new Comparator<Book>() {
@Override
public int compare(Book a, Book b) {
return a.getISBN().compareTo(b.getISBN());
}
}
What type does the compiler infer for a and b in your lambda, and why?
Thought process
Strip the wrapper and the single return statement. The body collapses to a.getISBN().compareTo(b.getISBN()). That is an expression, so no braces or return keyword. For the types: the surrounding context must declare Comparator<Book> (or a method accepting it); the compiler reads that and assigns type Book to both parameters via target typing (JLS §15.27.3).
Show answer
(a, b) -> a.getISBN().compareTo(b.getISBN())
The compiler infers both a and b as type Book. It reads the target type from wherever this lambda is used: for example, Arrays.sort(books, (a, b) -> ...) resolves Comparator<? super Book>, which pins T to Book, which makes compare(Book a, Book b) the method being implemented. (Source: Bloch EJ3 Item-42; JLS §15.27.3)
Level 3
Problem: Write a block-body lambda that sorts Player[] first by team name ascending, then (as a tiebreaker) by games played ascending. The result must be passed directly to Arrays.sort.
Explain why an expression body cannot be used here.
Thought process
Two separate comparisons mean two statements. An expression body holds exactly one expression and no semicolons. You need the block form with { } and two return statements. Compare byTeam first; if it is non-zero, return it; otherwise fall through to compare games played. Integer.compare handles the numeric field; String.compareTo handles the team name.
Show answer
Arrays.sort(players, (a, b) -> {
int byTeam = a.getTeam().compareTo(b.getTeam());
if (byTeam != 0) return byTeam;
return Integer.compare(a.getGamesPlayed(), b.getGamesPlayed());
});
An expression body cannot be used because the body requires two statements (the intermediate variable assignment plus the conditional return). An expression body is a single expression with one value; there is nowhere to put the if. (Source: JLS §15.27.2; canonical skeleton from README.md)
Go Deeper (optional)
None of what follows is needed to write correct lambda comparators. Read it only when you are curious about how the pieces fit into a larger picture.
How the compiler knows the types. The compiler performs “target typing” (JLS §15.27.3): it walks up the call chain to find the expected interface type, then uses that type to resolve the lambda’s parameter types and return type. This is the same inference engine that powers generic method calls. When you write Arrays.sort(players, (a, b) -> ...), the compiler sees Comparator<? super Player> in the second slot, pins T to Player, and hands both a and b the type Player without you writing it. The whole process is a form of type propagation that flows from the call site inward to the lambda body.
Lambdas are not full closures. In Python or Haskell a closure can capture and mutate variables from its enclosing scope. A Java lambda cannot: it can only refer to local variables that are effectively final, meaning the variable is never reassigned after the lambda is created. The JLS §15.27.4 specifies these capture rules precisely. The restriction exists because of the JVM’s stack discipline: a local variable lives on the stack frame of the method that created it, and that frame may be gone by the time the lambda runs. Capturing only final values sidesteps the problem cleanly. This constraint rarely matters for comparators (which typically compare fields on their two parameters rather than closing over locals), but it is the reason you cannot, for instance, accumulate a count inside a lambda variable and expect it to persist.
Where this shows up in real code. Lambda-based comparators appear in virtually every Java codebase built after 2015. Being able to read and write them fluently is a baseline expectation in industry code reviews and a routine question in technical interviews. You will encounter them immediately in any internship that touches Java.
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 expression-body lambda for a Comparator<Player> that sorts by games played ascending?
When you write Arrays.sort(players, (a, b) -> ...) and players is a Player[], what type does the compiler assign to a and b, and why?
Predict whether the following compiles, and if not, state the reason: (a, b) -> { a.getTeam().compareTo(b.getTeam()) } (Assume a and b are Player objects with a getTeam() method returning String.)
You need to translate this anonymous-class Comparator to a lambda: new Comparator<Book>() { @Override public int compare(Book a, Book b) { return a.getISBN().compareTo(b.getISBN()); } } Which translation is correct?
A student writes a block-body lambda with two sort keys: (a, b) -> { int byTeam = a.getTeam().compareTo(b.getTeam()); if (byTeam != 0) return byTeam; return Integer.compare(a.getGamesPlayed(), b.getGamesPlayed()); } For two Player objects where both have team “Hawks” and games played 10 vs 20, what int does the lambda return?