← Skill tree CS Skill Tree 0 CSCD211

When to Still Use the Anonymous-Class Form

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. Lambda vs. anonymous class. A lambda and an anonymous class both implement Comparator<Player> at the call site. What does a lambda NOT have that an anonymous class does?

Check

A lambda cannot declare fields or a Javadoc comment. It is an expression, not a class declaration. An anonymous class can have private fields and methods in its body.

2. Effectively final capture. An anonymous class (like a lambda) can capture local variables from the enclosing scope. What restriction applies?

Check

The captured local must be effectively final: not assigned after the capture point. Mutating it from inside the anonymous class is a compile error (JLS §8.1.3).

3. Functional interface. A lambda can be used anywhere the target type is a functional interface. What is a functional interface?

Check

An interface with exactly one abstract method. Comparator<T> qualifies because compare is its only abstract method (default methods and static methods do not count).


Try This First

You need a Comparator<Order> that both sorts by total price AND counts how many comparison calls have been made (for a performance test). Which form do you reach for first: a lambda, an anonymous class, or a named class?

Write your choice and one-sentence justification before reading further.

Compare your reasoning

An anonymous class with a private int comparisonCount field is the right choice here. A lambda cannot declare a field to accumulate the count across calls. A named class also works but is heavier for a comparator used at one call site. The anonymous class is the middle option: it adds a field without requiring a separate file.


What You Need To Walk In With

The Insight: A lambda is a pure anonymous function (an expression mapping inputs to an output, with no state). An anonymous class is an object that ALSO implements a function interface. When a comparator truly is a pure function with no persistent state, a lambda is shorter and clearer. When the comparator needs fields, Javadoc, or more than a few lines of logic, the anonymous class (or a named class) is the correct tool. The choice is not aesthetic; it follows from what the comparator must be able to do. You can: state the three reasons an anonymous class is still the right tool, choose among lambda, anonymous class, and named class for a given scenario, explain why a lambda cannot hold mutable state across compare calls, and predict the compile error produced when a captured local is not effectively final.


How It Works

Three legitimate reasons to keep the anonymous-class form

Reason 1: The body needs a field to hold mutable state across calls.

A lambda is an expression; it cannot declare fields. An anonymous class can:

Comparator<Order> instrumentedCmp = new Comparator<Order>() {
    private int callCount = 0;

    @Override
    public int compare(final Order a, final Order b) {
        callCount++;
        return Double.compare(a.getTotal(), b.getTotal());
    }

    public int getCallCount() { return callCount; }
};

Arrays.sort(orders, instrumentedCmp);
System.out.println("compare called " + instrumentedCmp.getCallCount() + " times");

A lambda version has no way to accumulate callCount across calls without an external AtomicInteger or a single-element array hack, both of which are awkward.

Reason 2: The compare method needs Javadoc.

A lambda is an expression; attaching a /** Javadoc */ block to it is impossible. An anonymous class body is a class declaration; its methods can have Javadoc. When the comparison logic is subtle enough to warrant documentation, the anonymous-class form (or a named class) is correct.

Reason 3: The interface has more than one abstract method.

A lambda can only target a functional interface (one abstract method). If you need to implement an interface with two abstract methods inline, only the anonymous-class form works.

For CSCD 211 lab comparators, none of these reasons apply. Every lab comparator is a one-liner that belongs in a lambda or a named class. This lesson exists so students recognize legitimate uses in production code.

The decision flowchart

Does the comparator need mutable state across calls, Javadoc, or implement a
multi-abstract-method interface?
    YES → anonymous class (or named class if reuse/testability needed)
    NO  → is the body a single expression?
              YES → lambda (or method reference if applicable)
              NO  → named class if reused; lambda with block body if one-off

Quick check

Check your understanding

A comparator must log each comparison to a file using a private PrintWriter field. Which form is correct for this call site?

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


Worked Example: Predict Then Check

A student writes:

int[] count = {0};   // effectively final array, mutable element
Comparator<Player> c = (a, b) -> {
    count[0]++;
    return a.getTeam().compareTo(b.getTeam());
};

Is this legal Java? Does it produce correct results?

Reasoning

count is an array reference, which is effectively final (the array itself is never replaced, only an element is mutated). Java allows this. The element count[0] is incremented on each call to compare. The results are correct.

However: this is a code smell. The idiomatic solution for instrumenting a comparator is an anonymous class with a private field. The single-element array workaround is the classic escape valve for the effectively-final capture restriction, and reviewers will ask why an anonymous class was not used instead.

Show answer

Legal: yes. The lambda compiles because count (the array reference) is effectively final.

Correct: yes, assuming the goal is to count comparisons and the array element is not reset between calls.

Idiomatic: no. The clean form for a stateful comparator is an anonymous class with a private field, as shown in “Reason 1” above.


Common Misconceptions

Misconception 1: continuing to write anonymous-class comparators out of habit

Wrong mental model: “I learned anonymous classes first; they always work, so I will keep using them for all comparators.”

Why it matters: In modern Java (Java 8+), code reviewers read anonymous-class comparators as a signal that the author is not familiar with lambdas. For a one-liner comparator, the anonymous form is at least 5 lines longer than the lambda form with no benefit. Using lambdas is not just shorter; it is the idiomatic modern style.

How to correct: Use lambdas for one-liner comparators. Use method references when the lambda body is just a single method call. Reserve anonymous classes for the three cases listed in this lesson.

Source: Bloch, Effective Java, Item 42.


Misconception 2: trying to capture a mutable local variable in a comparator body

Wrong mental model: “I will declare int counter = 0 outside the lambda, increment it inside, and read it after the sort.”

Why it breaks:

int counter = 0;
Comparator<Player> c = (a, b) -> {
    counter++;   // COMPILE ERROR: variable counter must be effectively final
    return a.getTeam().compareTo(b.getTeam());
};

The same restriction applies to anonymous classes: a captured variable must be effectively final. The workaround (single-element array or AtomicInteger) works but signals a design mismatch. The right tool for a stateful comparator is an anonymous-class field, not a captured local.

Source: JLS §8.1.3; Bloch, Effective Java, Item 42.


Quick check

Check your understanding

A student writes ‘Arrays.sort(players, new Comparator<Player>() { @Override public int compare(final Player a, final Player b) { return a.getLastName().compareTo(b.getLastName()); } });’. What does a code reviewer reading modern Java infer?

Tier 2 · Bloch, Effective Java, Item 42


Formal Definition and Interface Contract

From JLS §8.1.3 (Inner Classes and Enclosing Instances):

A local variable used but not declared in an inner class [...] must be declared final, or be effectively final.

From Bloch, Effective Java, Item 42:

Don’t use anonymous classes for function objects if you’re targeting Java 8 or later unless you have a specific reason that a lambda cannot satisfy. [...] Lambdas lack names and documentation. If a computation isn’t self-explanatory, or exceeds a few lines, don’t put it in a lambda. One line is ideal for a lambda, and three lines is a reasonable maximum.


Mental Model

Think of the three forms as tools of increasing weight:

Pick the lightest tool that does the job.


Connections

Within CSCD 210/211: This lesson is a design-choice lesson that bridges Anonymous-Class Syntax for Comparator and Writing a Comparator as a Lambda. In CSCD 211 labs, all comparators belong in lambdas or named classes. This lesson explains why, not just where.

Looking back: Anonymous-Class Syntax for Comparator covered the syntax of the anonymous-class form. This lesson covers the WHEN.

Looking ahead: The lambda lessons introduce lambdas as the idiomatic modern replacement. One Class, Many Comparators by Design covers the named-class form for comparators shared across many call sites.


Practice

Level 1

For each scenario, choose the correct form (lambda, anonymous class, or named class) and give a one-sentence reason:

(a) Sort a Player[] by last name at one call site. The body is one line.

(b) Sort Order[] by total price. The comparator must count how many times it is called.

(c) Sort Product[] by SKU. The same ordering is used in five different methods across two classes.

Show answers

(a) Lambda. The body is a single expression; no state or Javadoc is needed.

(b) Anonymous class with a private int callCount field. A lambda cannot accumulate state across calls.

(c) Named class (ProductBySkuComparator). Reuse across call sites in multiple classes makes a named class the right weight.


Level 2

A colleague asks you to review this code:

Arrays.sort(players, new Comparator<Player>() {
    @Override
    public int compare(final Player a, final Player b) {
        return a.getLastName().compareTo(b.getLastName());
    }
});

State what the reviewer’s comment should be and provide the corrected line.

Show answer

Reviewer comment: The body is a single expression with no state and no Javadoc need. Use a lambda.

Corrected:

Arrays.sort(players, (a, b) -> a.getLastName().compareTo(b.getLastName()));

Or, with a method reference:

Arrays.sort(players, Comparator.comparing(Player::getLastName));

Level 3

Design a CountingComparator<T> using the anonymous-class form that wraps any Comparator<T> and counts how many comparisons it performs. Write the code for the comparator and the sort call. Then explain why a lambda cannot implement this pattern directly.

Show answer
Comparator<Player> base = Comparator.comparing(Player::getLastName);

Comparator<Player> counting = new Comparator<Player>() {
    private int count = 0;

    @Override
    public int compare(final Player a, final Player b) {
        count++;
        return base.compare(a, b);
    }

    public int getCount() { return count; }
};

Arrays.sort(players, counting);
// safe cast because we know the runtime type
System.out.println("Comparisons: " + ((Object) counting).getClass()
    .getMethod("getCount").invoke(counting));

A cleaner approach stores the counting comparator in a helper class, but the anonymous-class form above demonstrates the core capability.

Why a lambda cannot do this: A lambda is an expression with no field declarations. There is nowhere to store the running count between calls. The single-element array trick (int[] count = {0}) requires the array to be declared outside the lambda, which works but is not encapsulated inside the comparator object.


Go Deeper (optional)

None of what follows is needed to write correct code. Read on only if you are curious where these ideas connect to the larger world of software design.

The distinction between a lambda and an anonymous class is, at its root, a distinction about state. A lambda is a pure function: the same inputs always produce the same output, and nothing is stored on the comparator between calls. An anonymous class is an object: it carries fields and therefore the comparator can remember something across invocations. This maps cleanly onto a principle in functional programming where closures close over values rather than mutable bindings. Java’s effectively-final capture restriction (JLS §8.1.3) is the language’s way of enforcing that a lambda does not silently mutate its captured environment, keeping the pure-function contract as tight as the language can manage.

The anonymous class as a comparator is also the simplest example of the Strategy pattern (a behavioral pattern from the Gang of Four, 1994): the sort algorithm is fixed, and the comparison strategy is passed in as an object. That strategy object can carry state (Reason 1 above) or configuration. When the strategy never needs state, a lambda is the leaner expression of the same idea and it is how Java 8 collapsed much of the boilerplate that Strategy once required. A lambda is, in practice, the smallest possible function object.

Looking at the full hierarchy that Bloch names in Effective Java Item 43: method reference > lambda > anonymous class > named class. Each step toward the left is more compact; each step toward the right adds a capability (a name, a field, a file, testability). The anonymous class sits in the middle exactly because it adds the one thing a lambda cannot have, which is a field, while avoiding the overhead of a full separate file. In professional Java codebases, code review tools and IDE inspections flag unnecessary anonymous-class comparators automatically because the pattern is recognized as pre-Java-8 style.

The career-relevant version of this is direct: if you apply for a Java role and a reviewer sees you writing anonymous-class comparators for simple one-liner sorts, they will note it. Knowing the three legitimate exceptions protects you from the opposite error too, which is blindly converting a stateful anonymous class to a lambda and breaking the counter or log it was maintaining.


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

According to this lesson, which of the following is a legitimate reason to choose an anonymous class over a lambda for a Comparator?

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

A code reviewer sees this code: Arrays.sort(players, new Comparator<Player>() { @Override public int compare(final Player a, final Player b) { return a.getLastName().compareTo(b.getLastName()); } }); What comment should the reviewer leave?

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

What happens when you compile this code? int counter = 0; Comparator<Player> c = (a, b) -> { counter++; return a.getTeam().compareTo(b.getTeam()); };

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

The lesson describes three tools in order of increasing weight: lambda, anonymous class, and named class. Which capability does moving from a lambda to an anonymous class add?

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

A student writes: int[] count = {0}; Comparator<Player> c = (a, b) -> { count[0]++; return a.getTeam().compareTo(b.getTeam()); }; Does this compile? Is it idiomatic?

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