Skip to content
Ameen Abozaid
PublishedBEG-035-L05

Demo: replacing anonymous classes with lambdas

  • Lesson 5 of 5
  • 11 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

This is a worked demo. You will take code written the long, noisy way and shrink it in small, safe steps, using everything from this module: lambdas, the standard functional interfaces, and method references. You will also hit, on purpose, the most common compiler error with lambdas, and see two ways out of it.

In the recipe-card picture, this lesson is a cleaning session: we start with cards cluttered with forms and stamps, and we trim each one down to the line that matters, checking after every step that the dish still tastes the same.

Why this matters: real codebases contain both styles. Older code (and some tutorials) use anonymous classes and copy-pasted loops, and new code uses lambdas. Refactoring from one to the other without changing behavior is a daily skill, and it is exactly what the lab "Filters" (BEG-035-LAB1) asks of you.

This lesson builds on all four earlier lessons in the module, and on Comparator from BEG-032.

Learning Objectives

By the end of this lesson you will be able to:

  • Convert an anonymous Comparator into a lambda and then into a comparator built from a method reference.
  • Replace a hard-coded filter loop with one method that takes a Predicate.
  • Check that a refactoring kept the behavior the same.
  • Recognize and explain the "effectively final" error in a realistic counting task.
  • Fix that error by restructuring, and explain why a mutable holder such as AtomicInteger is a last resort.
  • Compare the before and after code in lines and clarity.

Written Explanation

The starting point

We have a small Contact class with a name and an age:

java
static class Contact {
    private final String name;
    private final int age;
    Contact(String name, int age) { this.name = name; this.age = age; }
    String getName() { return name; }
    int getAge() { return age; }
    @Override public String toString() { return name + "(" + age + ")"; }
}

and a list we will reuse:

java
List<Contact> contacts = new ArrayList<>(List.of(new Contact("Sara", 12),
        new Contact("Omar", 34), new Contact("Amal", 17), new Contact("Adam", 52)));

The list is wrapped in new ArrayList<>(...) because List.of is unmodifiable and we want to sort it in place (BEG-030).

Step 0: the rule of refactoring

Before changing anything, record what the code does now (the output). After each step run it again and compare. If the output changes, the last step was not a refactoring. This is the habit that makes shrinking code safe.

Step 1: the anonymous Comparator

java
Collections.sort(contacts, new Comparator<Contact>() {
    @Override
    public int compare(Contact a, Contact b) {
        return a.getName().compareTo(b.getName());
    }
});
System.out.println(contacts);

Output:

text
[Adam(52), Amal(17), Omar(34), Sara(12)]

Seven lines for one idea: "compare by name".

Step 2: the lambda

Collections.sort expects a Comparator<Contact>, a functional interface, so the entire anonymous class collapses to its one method body. We use list.sort, the instance method you know from BEG-032:

java
contacts.sort((a, b) -> a.getName().compareTo(b.getName()));

That is one line. To prove the lambda really does the job, here is the same call written so the order is reversed (b before a):

java
contacts.sort((a, b) -> b.getName().compareTo(a.getName()));

Output after this reversed version:

text
[Sara(12), Omar(34), Amal(17), Adam(52)]

Step 3: the method reference and the builder

The lambda still repeats the idea "get the name" twice. Comparator.comparing takes a key extractor, a Function, so we can pass a method reference:

java
contacts.sort(Comparator.comparing(Contact::getName));
text
[Adam(52), Amal(17), Omar(34), Sara(12)]

Same result as step 1, now in a line that reads "sort by contact name". Here is the whole evolution side by side:

VersionLinesWhat the reader must parse
Anonymous class7class, annotation, method signature, parameter types, body
Lambda1two parameters and one comparison
Comparator.comparing + reference1the property being sorted

Each step removed noise and kept the output identical. For a numeric key such as age, the builder Comparator.comparingInt(Contact::getAge) avoids boxing.

Step 4: from a hard-coded loop to a Predicate

Now the filtering code. The "before" version has the condition fixed inside the loop:

java
static List<Contact> adultsLoop(List<Contact> in) {
    List<Contact> out = new ArrayList<>();
    for (Contact c : in) {
        if (c.getAge() >= 18) {
            out.add(c);
        }
    }
    return out;
}

If you also need "names starting with A", you would copy it. Instead, make the condition a parameter:

java
static List<Contact> keep(List<Contact> in, Predicate<Contact> p) {
    List<Contact> out = new ArrayList<>();
    for (Contact c : in) {
        if (p.test(c)) {
            out.add(c);
        }
    }
    return out;
}
java
System.out.println(adultsLoop(contacts));
System.out.println(keep(contacts, c -> c.getAge() >= 18));
System.out.println(keep(contacts, c -> c.getName().startsWith("A")));
text
[Adam(52), Omar(34)]
[Adam(52), Omar(34)]
[Adam(52), Amal(17)]

The first two lines are identical: the refactoring preserved behavior. The third line is new behavior that cost one line, not a whole new method.

Step 5: the planned mistake

Suppose you want to count adults, and, in the spirit of the lesson, you reach for forEach and a lambda:

java
int count = 0;
ages.forEach(x -> { if (x >= 18) { count++; } });
System.out.println(count);

The compiler stops you:

text
Bad.java:6: error: local variables referenced from a lambda expression must be final or effectively final
        ages.forEach(x -> { if (x >= 18) { count++; } });
                                           ^
1 error

You met this rule in the lambda syntax lesson. The lambda wants to change a local variable owned by the method, and Java forbids it. There are two ways out.

Fix 1, restructure (preferred). Counting is not what forEach is for. A plain loop is fine, and so is the keep method you already have:

java
int count = 0;
for (Contact c : contacts) {
    if (c.getAge() >= 18) {
        count++;
    }
}
// or, reusing what we built:
int count2 = keep(contacts, c -> c.getAge() >= 18).size();

Fix 2, a mutable holder (use sparingly). The restriction is on the variable, not on the object it points to. An AtomicInteger is an object whose value can change, so the variable counter itself is never reassigned:

java
AtomicInteger counter = new AtomicInteger();
contacts.forEach(c -> {
    if (c.getAge() >= 18) {
        counter.incrementAndGet();
    }
});
System.out.println(counter.get());

AtomicInteger comes from java.util.concurrent.atomic and is designed for counters shared between threads; the concurrency modules explain it. Treat it here as a preview. It works, but it is a workaround that hides the real problem (a lambda that changes outside state). With the fix-1 code all three versions printed 2.

The final comparison

The refactoring achieved the lab's acceptance rule, same behavior with less code:

  • Sort by name: 7 lines became 1.
  • Each new filter: a new 8-line method became 1 line.
  • Less duplicated logic, so fewer places for bugs.

Stop shrinking when the code stops getting clearer. A lambda that has grown braces and several statements should become a method (see the earlier lesson on lambda syntax), and a reference that puzzles readers should become a lambda or a named method.

Examples

Example 1: a tiny conversion (simple)

Before:

java
Runnable r = new Runnable() {
    public void run() { System.out.println("tick"); }
};

After:

java
Runnable r = () -> System.out.println("tick");

Both print tick when r.run() is called.

Example 2: sorting by age, two directions (practical)

java
contacts.sort(Comparator.comparingInt(Contact::getAge));
System.out.println(contacts);
contacts.sort(Comparator.comparingInt(Contact::getAge).reversed());
System.out.println(contacts);
text
[Sara(12), Amal(17), Omar(34), Adam(52)]
[Adam(52), Omar(34), Amal(17), Sara(12)]

Compare this with writing two anonymous classes, one for each direction.

Example 3: the whole refactoring as a checklist (realistic)

You are given a service method with an anonymous comparator and two filter loops. Work in this order:

  1. Run the code or its tests and keep the output.
  2. Replace each anonymous class with a lambda. Run again.
  3. Where the lambda only calls one method, use a method reference or a Comparator builder. Run again.
  4. Merge near-duplicate loops into one method with a Predicate parameter. Run again.
  5. Search for any lambda that changes a local variable, and restructure it.
  6. Compare before and after line counts, and write down what became clearer.

This is exactly the process of the lab, and it works because every step is small and checked.

Backend Insight

Refactoring toward lambdas is rarely about cleverness. It is about making the what visible: "sort by name", "keep adults". In a service class, such lines read like documentation, and a reviewer can check them quickly.

Two cautions for real projects. First, never refactor without a safety net: a test, or at least recorded output, as in step 0. Second, do not use lambdas to hide mutable state. The counter error was the compiler pointing at a real design smell: a callback that changes shared data is easy to get wrong when the callback runs on another thread, which is common in backend code. Prefer lambdas that take input and return output, and gather results with the right tool (a loop now, streams and collectors in BEG-036).

Common Mistakes

Mistake 1: Changing behavior while "just cleaning up"

Swapping a/b in a comparator or >= for > changes the result. Compare output after every step.

Mistake 2: Turning a long anonymous class into a long lambda

If the anonymous class had ten lines of logic, the lambda will too. Extract a method instead, and pass a lambda or reference that calls it.

Mistake 3: Reaching for AtomicInteger or an array to dodge the compiler

It compiles, but the compiler was pointing at a design problem. First ask whether a loop or a different method fits the job.

Mistake 4: Converting anonymous classes that need their own state or this

An anonymous class can have fields and this refers to itself; a lambda cannot hold its own fields, and this means the enclosing object. If the anonymous class stores state, keep it or turn it into a proper class.

Mistake 5: Forgetting that List.of cannot be sorted

List.of(...).sort(...) throws UnsupportedOperationException at runtime. Copy into an ArrayList first.

Exercises

Try each exercise yourself before opening the solutions. Use the Contact class and the keep method above.

Level A: Foundation

A1. Rewrite this anonymous class as a lambda:

java
Runnable r = new Runnable() {
    public void run() { System.out.println("done"); }
};
Show solution A1

A1. Runnable r = () -> System.out.println("done");

A2. Rewrite contacts.sort((a, b) -> a.getName().compareTo(b.getName())); using Comparator.comparing and a method reference.

Show solution A2

A2. contacts.sort(Comparator.comparing(Contact::getName));

A3. In "sort by name with a method reference", which functional interface does sort take, and which one does comparing take?

Show solution A3

A3. List.sort takes a Comparator<Contact>; Comparator.comparing takes a Function (the key extractor) and returns a Comparator.

Level B: Application

B1. Sort contacts from oldest to youngest, using a comparator builder.

Show solution B1

B1. contacts.sort(Comparator.comparingInt(Contact::getAge).reversed()); For the list above the result is [Adam(52), Omar(34), Amal(17), Sara(12)].

B2. Use keep to get contacts whose name has exactly four letters and who are under 18.

Show solution B2

B2. keep(contacts, c -> c.getName().length() == 4 && c.getAge() < 18). With the sample data this returns Sara and Amal (both are four letters and under 18).

B3. A teammate writes int minors = 0; contacts.forEach(c -> { if (c.getAge() < 18) minors++; });. State the compiler's complaint and fix the code without a lambda.

Show solution B3

B3. The complaint is "local variables referenced from a lambda expression must be final or effectively final", because minors++ changes a captured local. Fix:

java
int minors = 0;
for (Contact c : contacts) {
    if (c.getAge() < 18) {
        minors++;
    }
}

Level C: Challenge

C1. Write a new method static <T> int countIf(List<T> in, Predicate<T> p) and use it to count minors, so the original forEach problem disappears. Which earlier lesson's helper is it similar to?

Show solution C1

C1.

java
static <T> int countIf(List<T> in, Predicate<T> p) {
    int n = 0;
    for (T t : in) {
        if (p.test(t)) {
            n++;
        }
    }
    return n;
}
// countIf(contacts, c -> c.getAge() < 18)  -> 2

It is the count helper from the standard-interfaces lesson, the same shape as keep but counting instead of collecting. The counter variable lives inside the method, where a normal loop may change it.

C2. Write a short before/after summary (three bullet points) for a refactoring that replaced two anonymous comparators and two filter loops. What must stay the same, and what should shrink?

Show solution C2

C2.

  • Behavior must be identical: same sorted order and same lists, checked by comparing output before and after.
  • Four anonymous classes or loops of about 7 to 8 lines each should shrink to one-liners plus a single keep method.
  • Each remaining lambda should be a single clear idea, and any logic that needed braces should have become a named method.

Mini Challenge

This is practice for the lab. Write (or copy from your own earlier work) a short program with the following "before" code:

  1. A list of at least five Contact or Product objects.
  2. An anonymous Comparator that sorts by one field, and another that sorts by a second field.
  3. Two loops that each filter the list with a different condition.

Save a copy of this "before" version. Then refactor in the order of the checklist in Example 3 and record the output after each step. Finally, count the lines before and after and write one sentence on what got clearer.

Check your work: every output matches the "before" version, no lambda changes a local variable, and none of your lambdas needs braces.

Summary & Next Lesson

Key takeaways

  • Refactor in small steps and compare the output after each one.
  • An anonymous class that implements a functional interface becomes a lambda, and often a method reference or builder such as Comparator.comparing(Contact::getName).
  • Loops that differ only in a condition merge into one method with a Predicate parameter.
  • Changing a local variable inside a lambda is a compile error. Restructure first; a mutable holder such as AtomicInteger is a last resort (preview of the concurrency modules).
  • Shrink until the code is clearer, not just shorter.

Next lesson

This is the last lesson of the module. Next, the lab "Filters" (BEG-035-LAB1) asks you to apply this process yourself. After the module, the course continues with BEG-036, "Streams", whose first lesson is "Streams vs loops".

Self-check

  1. In what order do you convert an anonymous Comparator?
  2. What do you check after every refactoring step?
  3. Why did count++ in a lambda fail to compile?
  4. Name one better fix than AtomicInteger.
  5. When should you stop shrinking a lambda?
Show answers
  1. First to a lambda, then, where it only calls one method, to a method reference or a Comparator builder.
  2. That the output (behavior) is unchanged.
  3. count is a captured local variable that the lambda tries to change, so it is not effectively final.
  4. A plain loop, or a helper such as countIf with a Predicate.
  5. When the next shrink would make the code harder to read, or when the lambda needs braces and several statements, which means it should be a named method.

References

  • Lambda Expressions, The Java Tutorials (Oracle). Supports: replacing anonymous classes with lambdas, the final-or-effectively-final capture rule, and the difference in this between anonymous classes and lambdas.
  • Method References, The Java Tutorials (Oracle). Supports: using a method reference where a lambda only calls an existing method, such as the key extractor passed to Comparator.comparing.
  • All code and the compiler message in this lesson were run with JDK 21; the outputs shown are real. The description of AtomicInteger as a thread-safe counter was not verified against its documentation in this session, so treat it as a preview that the concurrency modules will cover properly.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Type to search levels, phases, modules and lessons.