The previous lesson ended with a legitimate annoyance: Catalog.ByTitle is a complete class — declaration, @Override, body, name and a slot in the namespace — for a single line of logic used in exactly one place. When the code you want to write fits on one line and the ceremony wrapping it takes five, something is redundant.

Anonymous classes remove that ceremony. They let you declare an implementation and instantiate it in the same expression, at the exact point where it is needed and without giving it a name. For fifteen years they were Java's standard mechanism for passing behaviour to a method: every Comparator, every GUI listener, every Runnable was written that way. Today lambdas have taken over much of that ground, but anonymous classes are still the only option in several specific scenarios, and above all they still appear throughout all existing Java code: reading them fluently is not optional. Besides, understanding them thoroughly is the best possible preparation for lesson 04-05, because a lambda is nothing more than an anonymous class with everything dispensable stripped away.

Contents

  1. What an anonymous class is
  2. The syntax, dissected
  3. What it can and cannot do
  4. The instance initialisation block
  5. Its own methods: they exist, but they are almost useless
  6. Capturing effectively final variables
  7. this inside an anonymous class
  8. Classic use 1: a Comparator for the catalogue
  9. Classic use 2: a callback
  10. Classic use 3: extending a class on the spot
  11. When it is still the right option and when to use a lambda
  12. "Double brace initialization" and why to avoid it
  13. Bytecode weight and readability
  14. Final decision table
  15. Common Mistakes and Tips
  16. Exercises

  1. What an anonymous class is

An anonymous class is a class without a name that is declared and instantiated in a single expression. It can be:

  • An implementation of an interface, or
  • A subclass of a class (concrete or abstract).

Compare the two ways of obtaining a comparator by title:

// --- FORM 1: named nested class (04-03) ---
public static class ByTitle implements Comparator<Material> {
    @Override
    public int compare(Material a, Material b) {
        return a.getTitle().compareToIgnoreCase(b.getTitle());
    }
}
// ... and somewhere else:
Arrays.sort(catalog, new Catalog.ByTitle());
// --- FORM 2: anonymous class, right here ---
Arrays.sort(catalog, new Comparator<Material>() {
    @Override
    public int compare(Material a, Material b) {
        return a.getTitle().compareToIgnoreCase(b.getTitle());
    }
});

The second form declares no named class, takes up no place in Catalog's namespace and puts the logic exactly where it is used. Whoever reads Arrays.sort(...) sees at once which criterion the sorting uses, without going off to find another file.

The expression new Comparator<Material>() { ... } does three things at once:

  1. It declares a new class implementing Comparator<Material>.
  2. It instantiates it.
  3. It returns that instance as the value of the expression.

  1. The syntax, dissected

This is the part worth examining under a magnifying glass, because it has a peculiar structure:

Comparator<Material> byTitle = new Comparator<Material>() {
    @Override
    public int compare(Material a, Material b) {
        return a.getTitle().compareToIgnoreCase(b.getTitle());
    }
};

Piece by piece:

Fragment What it is
Comparator<Material> byTitle = A normal declaration. The type is the interface, not the anonymous class (which has no name)
new An object is about to be created
Comparator<Material> The type being implemented (if an interface) or extended (if a class)
() Constructor arguments. Empty for an interface; if you extend a class, its arguments go here
{ ... } Body of the anonymous class: its methods and fields
; The final semicolon. The most forgotten one in Java

That final semicolon is the number one source of errors with anonymous classes. The reason is that the whole construct is an expression inside a statement, not a class declaration. Think of it this way: if you write int x = 5;, the ; is obvious. The same thing happens here; what throws you off is that there is a closing brace in the middle, and the eye reads it as the end of a class.

// With the final semicolon (correct):
Comparator<Material> c = new Comparator<Material>() { ... };

// Without it (compilation error):
Comparator<Material> c = new Comparator<Material>() { ... }

When the anonymous class goes in as a method argument, the closing parenthesis appears after the brace, something that looks jarring at first:

Arrays.sort(catalog, new Comparator<Material>() {
    @Override
    public int compare(Material a, Material b) {
        return a.getTitle().compareToIgnoreCase(b.getTitle());
    }
});   // brace, parenthesis, semicolon

That }); is the visual signature of an anonymous class in Java code. Learn to recognise it.

  1. What it can and cannot do

It can It cannot
Implement one interface Implement several interfaces
Extend one class (concrete or abstract) Extend a class and implement interfaces at the same time
Have its own fields Have its own constructor
Have an instance initialisation block Be abstract, static or final
Declare new methods Have those methods called from outside (section 5)
Access the outer class's members Be reused: it is single-use
Capture effectively final variables Modify those variables

The central limitation is having no constructor. And the reason is simple: a constructor carries the name of its class, and this class has no name. There is no way to write it.

Comparator<Material> c = new Comparator<Material>() {
    // Comparator() { }        // IMPOSSIBLE: the class has no name
    // AnonymousNoName() { }   // IMPOSSIBLE: that is not what it is called
    @Override public int compare(Material a, Material b) { return 0; }
};

And what if you extend a class that does have a constructor? Then you can pass it arguments in the new parentheses, which are forwarded to the superclass constructor:

// Extends Employee using its two-argument constructor
Employee intern = new Employee("Nuria Vidal", "EMP-003") {
    @Override
    public boolean canBorrow() {
        return false;   // interns on placement do not borrow materials
    }
};

Here ("Nuria Vidal", "EMP-003") is not the anonymous class's constructor: it is the invocation of Employee's constructor. The anonymous class gets an implicit constructor that merely forwards them.

  1. The instance initialisation block

If there is no constructor, how do you initialise a field with logic? With an instance initialisation block: a loose { ... } block in the class body, which runs when the object is constructed, right after the superclass constructor.

Comparator<Material> byTitleWithLog = new Comparator<Material>() {

    private int comparisons;            // own field
    private final String label;         // final field

    {   // INSTANCE INITIALISATION BLOCK (acts as the constructor)
        label       = "cmp-" + System.nanoTime();
        comparisons = 0;
        System.out.println("Comparator created: " + label);
    }

    @Override
    public int compare(Material a, Material b) {
        comparisons++;
        return a.getTitle().compareToIgnoreCase(b.getTitle());
    }
};

This mechanism is not exclusive to anonymous classes — any class can have one — but here it is the only way to run initialisation code. The full order, extending what you saw in 03-04, is:

flowchart TD
    A["new Anonymous(...)"] --> B["superclass constructor"]
    B --> C["field initialisers, in the order written"]
    C --> D["instance initialisation block"]
    D --> E["body of the implicit constructor (empty)"]
    E --> F["object ready"]

In practice, if your anonymous class needs a complex initialisation block, take it as a signal: it probably deserves to be a named class.

  1. Its own methods: they exist, but they are almost useless

An anonymous class can declare methods that are not in the interface or the base class. They compile. But there is a catch:

Comparator<Material> comparator = new Comparator<Material>() {

    private int comparisons;

    @Override
    public int compare(Material a, Material b) {
        comparisons++;
        return a.getTitle().compareToIgnoreCase(b.getTitle());
    }

    /** Its own method: NOT in Comparator. */
    public int getComparisons() {
        return comparisons;
    }
};

Arrays.sort(catalog, comparator);

// comparator.getComparisons();   // DOES NOT COMPILE
error: cannot find symbol
  symbol:   method getComparisons()
  location: variable comparator of type Comparator<Material>

The reason is the polymorphism rule from 03-06: the declared type decides what you can call. The variable's type is Comparator<Material>, and Comparator has no getComparisons(). And you cannot declare it with the actual type, because the actual type has no name.

There is a loophole, useful for understanding the mechanism even if you never use it: invoking the method on the expression itself, before the type is lost.

System.out.println(
    new Object() {
        int countLetters(String s) { return s.length(); }
    }.countLetters("Effective Java")
);   // 14

It works because the compiler knows the anonymous type as long as the expression lasts. As soon as you assign it to a variable, the type generalises and the method becomes unreachable.

Practical conclusion: an anonymous class's own methods are only useful inside it, as private helpers for the method that is part of the contract. If you need to expose new methods, you need a named class.

  1. Capturing effectively final variables

Exactly the same rule as for local classes applies (04-03): an anonymous class can read local variables and parameters of the surrounding method, provided they are final or effectively final.

public Material[] sortByFine(Material[] materials, int elapsedDays) {

    Material[] copy = Arrays.copyOf(materials, materials.length);

    Arrays.sort(copy, new Comparator<Material>() {
        @Override
        public int compare(Material a, Material b) {
            // 'elapsedDays' is a captured parameter: it is not reassigned
            return Double.compare(b.calculateFine(elapsedDays),
                                  a.calculateFine(elapsedDays));
        }
    });

    return copy;
}

If at any point in the method you wrote elapsedDays = 30;, the variable would stop being effectively final and the error would appear inside the anonymous class:

error: local variables referenced from an inner class must be final
       or effectively final

The reason is the one you studied in 04-03: the local variable lives on the stack and dies with the method; the anonymous object lives on the heap and can outlive it. Java copies the value inside the object, and allowing modification would produce two uncoordinated copies.

And an important note that avoids confusion: the restriction applies to the variable, not to the object. You can capture a reference and modify the object it points to:

Employee marta = new Employee("Marta Ruiz", "EMP-001");

Runnable task = new Runnable() {
    @Override
    public void run() {
        marta.registerLoan();        // ALLOWED: the OBJECT is modified
        // marta = otherEmployee;    // FORBIDDEN: the VARIABLE would be reassigned
    }
};

Runnable is the interface for runnable tasks; its real use with threads is module 8.

  1. this inside an anonymous class

This is the point that causes the most subtle errors in the module, and it deserves full attention.

Inside an anonymous class, this refers to the anonymous class itself, not to the class surrounding it.

package com.nexussoftware.bibliotech.service;

public class LoanManager {

    private final String managerName = "Central manager";

    public void demonstrateThis() {

        Runnable task = new Runnable() {

            private final String managerName = "Anonymous task";

            @Override
            public void run() {
                System.out.println("this.managerName        = " + this.managerName);
                System.out.println("LoanManager.this.name   = "
                                   + LoanManager.this.managerName);
                System.out.println("this.getClass()         = " + this.getClass().getName());
                System.out.println("LoanManager.this.class  = "
                                   + LoanManager.this.getClass().getName());
            }
        };

        task.run();
    }
}
this.managerName        = Anonymous task
LoanManager.this.name   = Central manager
this.getClass()         = com.nexussoftware.bibliotech.service.LoanManager$1
LoanManager.this.class  = com.nexussoftware.bibliotech.service.LoanManager

The last two lines prove it beyond doubt: this is an object of the class LoanManager$1, the anonymous class the compiler generated (04-03, section 9).

To reach the outer instance you have to write OuterClass.this, the same syntax as with inner classes.

Why does it matter so much? Because the typical mistake is silent:

public class Catalog {

    private String lastCriterion;

    public void sortByTitle(Material[] materials) {
        Arrays.sort(materials, new Comparator<Material>() {
            @Override
            public int compare(Material a, Material b) {
                // This DOES work: no ambiguity, the field only exists outside
                lastCriterion = "title";
                return a.getTitle().compareToIgnoreCase(b.getTitle());
            }
        });
    }
}

That access works because there is no lastCriterion in the anonymous class. But as soon as the anonymous class declares a field with the same name — or you call a record() method that exists in both — the resolution changes destination without warning and the program does something different from what you expected. The resolution rule, just as in 04-03: local variable → member of the anonymous class → member of the outer class.

Keep this table, because in 04-05 you are going to compare it with the behaviour of lambdas and you will see it is exactly the opposite:

Expression Inside an anonymous class
this The anonymous instance
this.field Field of the anonymous class
Outer.this The instance of the enclosing class
this.getClass() Outer$1
A bare name without this The nearest one: local, then anonymous, then outer

  1. Classic use 1: a Comparator for the catalogue

The most frequent use. Here is the Catalog from 04-03 rewritten with anonymous classes, plus a third, composite criterion:

package com.nexussoftware.bibliotech.service;

import com.nexussoftware.bibliotech.domain.Material;
import java.util.Arrays;
import java.util.Comparator;

public class Catalog {

    private final Material[] materials;

    public Catalog(Material[] materials) {
        this.materials = Arrays.copyOf(materials, materials.length);
    }

    private Material[] copy() {
        return Arrays.copyOf(materials, materials.length);
    }

    /** Alphabetical order by title. */
    public Material[] sortedByTitle() {
        Material[] c = copy();
        Arrays.sort(c, new Comparator<Material>() {
            @Override
            public int compare(Material a, Material b) {
                return a.getTitle().compareToIgnoreCase(b.getTitle());
            }
        });
        return c;
    }

    /** Highest fine first. The parameter is captured: it is effectively final. */
    public Material[] sortedByFine(int elapsedDays) {
        Material[] c = copy();
        Arrays.sort(c, new Comparator<Material>() {
            @Override
            public int compare(Material a, Material b) {
                return Double.compare(b.calculateFine(elapsedDays),
                                      a.calculateFine(elapsedDays));
            }
        });
        return c;
    }

    /** Composite criterion: first by type, and within each type by title. */
    public Material[] sortedByTypeAndTitle() {
        Material[] c = copy();
        Arrays.sort(c, new Comparator<Material>() {
            @Override
            public int compare(Material a, Material b) {
                int byType = a.getType().compareTo(b.getType());
                if (byType != 0) {
                    return byType;               // main criterion
                }
                return a.getTitle().compareToIgnoreCase(b.getTitle());   // tie-breaker
            }
        });
        return c;
    }
}
Material[] data = {
    new Dvd("Refactoring Live", "DVD-0007", 95),
    new Book("Effective Java",  "Joshua Bloch",  "978-0000000001", 2018),
    new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"),
    new Book("Design Patterns", "Erich Gamma",   "978-0000000002", 1994),
    new Book("Refactoring",     "Martin Fowler", "978-0000000003", 1999)
};

Catalog catalog = new Catalog(data);

System.out.println("--- By type and title ---");
for (Material m : catalog.sortedByTypeAndTitle()) {
    System.out.printf("  %-10s %s%n", m.getType(), m.getTitle());
}
--- By type and title ---
  Book       Design Patterns
  Book       Effective Java
  Book       Refactoring
  DVD        Refactoring Live
  Magazine   Java Magazine

sortedByTypeAndTitle is a good example of an anonymous class that is still justified: it has several lines of logic with an intermediate condition. Sorting like this with a single compare is exactly what Comparator.comparing().thenComparing() will solve more elegantly in 04-06.

  1. Classic use 2: a callback

A callback is an object with behaviour that you pass to another one so that it invokes it when something happens. It is the mechanism that lets a service class notify without knowing whom.

package com.nexussoftware.bibliotech.service;

import com.nexussoftware.bibliotech.domain.Loan;

/** This listener is notified every time a return is registered. */
public interface ReturnListener {
    void onReturn(Loan loan, double fine);
}
package com.nexussoftware.bibliotech.service;

import com.nexussoftware.bibliotech.domain.Loan;

public class LoanManager {

    private ReturnListener listener;

    /** Registers who wants to be notified. */
    public void setListener(ReturnListener listener) {
        this.listener = listener;
    }

    public double processReturn(Loan loan, int elapsedDays) {
        double fine = loan.registerReturn(elapsedDays);
        if (listener != null) {
            listener.onReturn(loan, fine);         // callback
        }
        return fine;
    }
}

And now the usage, with two different listeners defined at the exact point where what to do is decided:

LoanManager manager = new LoanManager();

// Listener 1: console receipt
manager.setListener(new ReturnListener() {
    @Override
    public void onReturn(Loan loan, double fine) {
        System.out.printf("RECEIPT %s: %s returned by %s. Fine %.2f EUR%n",
                          loan.getReference(), loan.getMaterialTitle(),
                          loan.getEmployeeName(), fine);
    }
});
manager.processReturn(l1, 20);

// Listener 2: only alerts if the fine exceeds a threshold
final double threshold = 5.0;
manager.setListener(new ReturnListener() {
    @Override
    public void onReturn(Loan loan, double fine) {
        if (fine > threshold) {                   // capture of 'threshold'
            System.out.printf("ALERT: high fine (%.2f EUR) on %s%n",
                              fine, loan.getReference());
        }
    }
});
manager.processReturn(l2, 40);
RECEIPT LN-0001: Effective Java returned by Marta Ruiz. Fine 1.25 EUR
ALERT: high fine (8.50 EUR) on LN-0002

Notice what has been achieved: LoanManager knows nothing about receipts or alerts. It only knows the ReturnListener interface and calls its method. It is the dependency inversion of 04-01 in action, and it is the basis of the Observer pattern you will formalise in 12-02.

  1. Classic use 3: extending a class on the spot

Anonymous classes also serve to create a one-off variant of an existing class, typically in tests:

// An employee with a different limit, just for this test
Employee manager = new Employee("Nuria Vidal", "EMP-004") {
    @Override
    public boolean canBorrow() {
        return true;    // no limit on concurrent loans
    }
};

for (int i = 0; i < 6; i++) {
    manager.registerLoan();
}
System.out.println(manager.getName() + " has taken 6 materials without a warning.");

And with abstract classes, which cannot be instantiated (04-02) but can be extended anonymously:

// A test material, with no need to create a new class
Material testMaterial = new Material("Internal Manual", "INT-0001", true) {
    @Override public String getType()      { return "Internal"; }
    @Override public int    getLoanDays()  { return 5; }
    @Override public double getDailyRate() { return 0.05; }
};

System.out.println(testMaterial.describe());
System.out.printf("Fine after 20 days: %.2f EUR%n", testMaterial.calculateFine(20));
Internal "Internal Manual" (ref. INT-0001)
Fine after 20 days: 0.75 EUR

This use — creating on the fly a minimal implementation of an abstract class to test the base logic — is very common in module 11 with JUnit. And note the detail: new Material(...) is forbidden, but new Material(...) { ... } is legal, because it does not instantiate Material but an anonymous subclass that is concrete.

  1. When it is still the right option and when to use a lambda

Java 8 brought lambdas, which express the same thing in far less space:

// Anonymous class
Arrays.sort(c, new Comparator<Material>() {
    @Override
    public int compare(Material a, Material b) {
        return a.getTitle().compareToIgnoreCase(b.getTitle());
    }
});

// Equivalent lambda (04-05)
Arrays.sort(c, (a, b) -> a.getTitle().compareToIgnoreCase(b.getTitle()));

But the lambda does not replace the anonymous class in every case. The rule is categorical:

A lambda can only implement an interface with a single abstract method (a functional interface, 04-06). For everything else, an anonymous class is still needed.

Situation Use
Interface with one single abstract method and a short body Lambda
Interface with two or more abstract methods Anonymous class
Extending a class (concrete or abstract) Anonymous class
You need your own fields with state between calls Anonymous class
You need an initialisation block Anonymous class
You need this to be the implementing object Anonymous class
Body longer than ~5 lines or with several branches Anonymous class or named method
It is used in more than one place Named class

An example of an irreplaceable anonymous class, with its own state between calls:

/** A comparator that counts how many times it is called. A lambda CANNOT do this. */
Comparator<Material> instrumented = new Comparator<Material>() {

    private int calls;         // its own STATE: impossible in a lambda

    @Override
    public int compare(Material a, Material b) {
        calls++;
        if (calls % 5 == 0) {
            System.out.println("  [" + calls + " comparisons]");
        }
        return a.getTitle().compareToIgnoreCase(b.getTitle());
    }
};

The full syntax of the lambda, its shorthand forms and its rules are the content of lesson 04-05. Here it is enough to know that it exists and when it will not do.

  1. "Double brace initialization" and why to avoid it

There is a widely circulated trick that combines an anonymous class with an initialisation block to initialise objects "inline". It is called double brace initialization because of the two consecutive braces:

// DO NOT DO THIS
Employee marta = new Employee("Marta Ruiz", "EMP-001") {{
    registerLoan();
    registerLoan();
}};

It reads like this: the first brace opens the anonymous class, the second opens an instance initialisation block. It looks elegant and compact. It is a recognised bad practice, for four concrete reasons:

  1. It creates an extra class per use. Each {{ }} generates one more .class file. A method called a thousand times with this syntax in a loop generates one class, yes, but a thousand instances of an unnecessary subclass.
  2. The object is no longer of the class you think. marta.getClass() returns App$1, not Employee. And since well-written equals methods compare with getClass() (03-09), marta.equals(otherEmployee) returns false even if they have the same identifier. It is a real bug and a hard one to find.
  3. It retains the outer instance. It is a non-static inner class: it drags along the hidden this$0 reference with everything that entails (04-03), including the memory leak if the object is stored in a cache.
  4. It breaks serialisation. An anonymous class does not serialise well (module 7).

The correct alternatives have been within your reach since module 3:

// Option A: simply write the lines
Employee marta = new Employee("Marta Ruiz", "EMP-001");
marta.registerLoan();
marta.registerLoan();

// Option B: a constructor that receives the initial state
Employee marta2 = new Employee("Marta Ruiz", "EMP-001", 2);

// Option C: a factory method with an expressive name
Employee marta3 = Employee.withLoans("Marta Ruiz", "EMP-001", 2);

  1. Bytecode weight and readability

Two costs worth keeping in mind.

One class per anonymous class. The compiler generates Outer$1.class, Outer$2.class, Outer$3.class... numbering them in order of appearance. Consequences:

  • More files in the .jar and more class-loading time at startup.
  • Cryptic error traces: at com.nexussoftware.bibliotech.service.Catalog$2.compare(Catalog.java:41). The name says nothing; you have to go to line 41 to know which comparator is meant.
  • Renumbering: if you add an anonymous class before another, all the following ones change number. In debugging this is disorienting.

Lambdas mitigate this, because they do not generate one class per lambda: they use the invokedynamic instruction and create the implementation at run time. It is one of their less well-known advantages and it is the reason lambdas do not make startup more expensive the way thousands of anonymous classes used to.

Readability. A three-line anonymous class improves reading, because it puts the logic where it is used. A thirty-line anonymous class wrecks the reading of the method containing it: the main flow is buried under a class declaration. The most useful informal rule:

If the anonymous class does not fit entirely on the screen together with the code around it, move it out to a named class.

  1. Final decision table

This is the summary of the module so far. Keep it: it answers the question "which form of class do I use?" in almost any real situation.

Form Use it when Avoid it when
Top-level class The type has substance of its own, is used from several places and is part of the domain It is a fifteen-line helper used by only one class
Static nested It is a helper conceptually tied to the outer class: nodes, builders, data groupings It needs the outer instance's state
Inner (non-static) You genuinely need access to the outer instance's state Almost always. Risk of a memory leak
Local A type with several fields and methods that only exists inside an algorithm It fits in a lambda or the method is already long
Anonymous A single-use implementation that needs state, several abstract methods or to extend a class The interface is functional and the body is short
Lambda (04-05) Functional interface, short body, no state of its own You need this, state or to extend a class
flowchart TD
    A["I need an implementation"] --> B{"Is it used in more than one place?"}
    B -- "Yes" --> C["Named class: top-level or static nested"]
    B -- "No" --> D{"Am I extending a class, or does the interface have several abstract methods?"}
    D -- "Yes" --> E["Anonymous class"]
    D -- "No" --> F{"Do I need my own state, or this to be the implementer?"}
    F -- "Yes" --> E
    F -- "No" --> G{"Does the body fit in a few lines?"}
    G -- "Yes" --> H["Lambda"]
    G -- "No" --> I["Named method and method reference"]

Common Mistakes and Tips

Forgetting the final semicolon. }; closes a statement, not a class. It is the number one error, and the compiler's message (';' expected) points at the following line, which is disorienting.

Trying to write a constructor. It does not exist. Use an instance initialisation block, or pass the arguments to the superclass constructor in the new parentheses.

Calling one of the anonymous class's own methods from outside. It does not compile: the variable has the type of the interface or the superclass. If you need to expose new methods, you need a name.

Believing that this is the outer class. It is the most expensive conceptual mistake. Inside the anonymous class, this is the anonymous class. Use Outer.this. And get ready for this to be exactly the other way round with lambdas in 04-05.

Modifying a captured local variable. Forbidden: it must be effectively final. But you can modify the object a captured reference points to.

Using double brace initialization. It changes the object's actual class, breaks equals based on getClass(), retains the outer instance and generates extra classes.

Giant anonymous classes. A thirty-line anonymous class inside a method call is an unreadable method. Move it out.

Tip: always put @Override. Just as with inheritance (03-05), it instantly detects a badly written signature. Without it, a mistyped compare(Material a, Object b) becomes a new method and the compiler complains much later and much worse.

Tip: name the variable if you are going to reuse the anonymous class. If the same comparator is used in three methods, assign it to a field private static final Comparator<Material> BY_TITLE = new Comparator<>() { ... };. A single instance is created and the name documents the criterion.

Tip: the diamond <> works from Java 9 onwards. new Comparator<>() { ... } is valid and saves repeating the type. Before Java 9 you had to write new Comparator<Material>() { ... }.

Exercises

Exercise 1: three anonymous comparators

Write a method static void showSorted(Material[] materials, String criterion, int days) that sorts a copy of the array with Arrays.sort using a different anonymous class depending on the criterion received: "title" (alphabetical ascending), "term" (shortest term first) and "fine" (highest fine first, for the given days). Use an arrow switch (02-03). Test all three with the BiblioTech catalogue.

Exercise 2: callback with state

Extend the LoanManager from section 9. Write an anonymous ReturnListener class that accumulates the number of returns and the total of fines in its own fields, and prints a summary every three returns. Explain why this anonymous class could not be a lambda.

Exercise 3: this in an anonymous class

Write a class Auditor with a field private String source = "Auditor" and a method audit() that creates an anonymous Runnable class with its own field source = "Task". Inside run(), print: the anonymous class's field, the outer class's field, the class name of this and the class name of Auditor.this. Predict the output before running it.

Solutions

Solution 1

package com.nexussoftware.bibliotech.service;

import com.nexussoftware.bibliotech.domain.*;
import java.util.Arrays;
import java.util.Comparator;

public class CatalogSorter {

    public static void showSorted(Material[] materials, String criterion, int days) {

        Material[] copy = Arrays.copyOf(materials, materials.length);

        // arrow switch (02-03) returning a different Comparator in each branch
        Comparator<Material> comparator = switch (criterion) {

            case "title" -> new Comparator<Material>() {
                @Override
                public int compare(Material a, Material b) {
                    return a.getTitle().compareToIgnoreCase(b.getTitle());
                }
            };

            case "term" -> new Comparator<Material>() {
                @Override
                public int compare(Material a, Material b) {
                    // Integer.compare avoids the overflow of 'a - b'
                    return Integer.compare(a.getLoanDays(), b.getLoanDays());
                }
            };

            case "fine" -> new Comparator<Material>() {
                @Override
                public int compare(Material a, Material b) {
                    // b before a: DESCENDING order
                    // 'days' is a captured parameter, effectively final
                    return Double.compare(b.calculateFine(days), a.calculateFine(days));
                }
            };

            default -> new Comparator<Material>() {
                @Override
                public int compare(Material a, Material b) {
                    return 0;      // no order: leaves the array as it is
                }
            };
        };

        Arrays.sort(copy, comparator);

        System.out.println("--- Sorted by " + criterion + " ---");
        for (Material m : copy) {
            System.out.printf("  %-10s %-24s term %2d  fine %5.2f EUR%n",
                              m.getType(), m.getTitle(),
                              m.getLoanDays(), m.calculateFine(days));
        }
    }

    public static void main(String[] args) {
        Material[] catalog = {
            new Dvd("Refactoring Live", "DVD-0007", 95),
            new Book("Effective Java",  "Joshua Bloch",  "978-0000000001", 2018),
            new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"),
            new Book("Design Patterns", "Erich Gamma",   "978-0000000002", 1994)
        };

        showSorted(catalog, "title", 20);
        showSorted(catalog, "term",  20);
        showSorted(catalog, "fine",  20);
    }
}
--- Sorted by title ---
  Book       Design Patterns          term 15  fine  1.25 EUR
  Book       Effective Java           term 15  fine  1.25 EUR
  Magazine   Java Magazine            term  7  fine  1.30 EUR
  DVD        Refactoring Live         term  3  fine  8.50 EUR
--- Sorted by term ---
  DVD        Refactoring Live         term  3  fine  8.50 EUR
  Magazine   Java Magazine            term  7  fine  1.30 EUR
  Book       Effective Java           term 15  fine  1.25 EUR
  Book       Design Patterns          term 15  fine  1.25 EUR
--- Sorted by fine ---
  DVD        Refactoring Live         term  3  fine  8.50 EUR
  Magazine   Java Magazine            term  7  fine  1.30 EUR
  Book       Effective Java           term 15  fine  1.25 EUR
  Book       Design Patterns          term 15  fine  1.25 EUR

Two technical notes. First: Integer.compare(a, b) and Double.compare(a, b) instead of manual subtractions; subtracting integers can overflow and subtracting doubles forces you to convert the result to int, losing precision. Second: the order between the two 15-day books stays as they were in the array, because Arrays.sort on objects is stable. That detail will matter a lot in 05-09.

And an observation anticipating the next lesson: all four bodies have exactly one line of useful logic, each wrapped in five lines of ceremony. In 04-05 this whole method will come down to four lines.

Solution 2

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.domain.*;
import com.nexussoftware.bibliotech.service.*;

public class ReturnSession {

    public static void main(String[] args) {

        LoanManager manager = new LoanManager();

        /*
         * This anonymous class CANNOT be a lambda because it keeps its own
         * STATE between calls (the fields 'returnCount' and 'totalFines').
         * A lambda is an implementation without fields: it can only read
         * captured variables, which must also be effectively final.
         */
        manager.setListener(new ReturnListener() {

            private int    returnCount;
            private double totalFines;

            {   // initialisation block: acts as the constructor
                System.out.println("Return session started.");
            }

            @Override
            public void onReturn(Loan loan, double fine) {
                returnCount++;
                totalFines += fine;

                System.out.printf("  %s returned: %.2f EUR%n",
                                  loan.getReference(), fine);

                if (returnCount % 3 == 0) {
                    System.out.printf("  >>> SUMMARY: %d returns, %.2f EUR accrued%n",
                                      returnCount, totalFines);
                }
            }
        });

        Employee marta = new Employee("Marta Ruiz",   "EMP-001");
        Employee diego = new Employee("Diego Alonso", "EMP-002");

        Material[] materials = {
            new Book("Effective Java",  "Joshua Bloch",  "978-0000000001", 2018),
            new Book("Design Patterns", "Erich Gamma",   "978-0000000002", 1994),
            new Dvd("Refactoring Live", "DVD-0007", 95),
            new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly")
        };
        int[] daysPerLoan = { 20, 16, 25, 30 };

        for (int i = 0; i < materials.length; i++) {
            Employee who = (i % 2 == 0) ? marta : diego;
            Loan l = new Loan(materials[i], who, 100);
            manager.processReturn(l, daysPerLoan[i]);
        }
    }
}
Return session started.
  LN-0001 returned: 1.25 EUR
  LN-0002 returned: 0.25 EUR
  LN-0003 returned: 11.00 EUR
  >>> SUMMARY: 3 returns, 12.50 EUR accrued
  LN-0004 returned: 2.30 EUR

Why it cannot be a lambda: a lambda has no fields. Its body can only use its parameters, the members of the enclosing class and captured local variables, and the latter must be effectively final, that is, they cannot be incremented. Here returnCount and totalFines change on every invocation and their value must persist between calls: that requires state in the object, and state in the object requires a class. Besides, this anonymous class uses an initialisation block, something that does not exist in a lambda either. It is the textbook case where the anonymous class is still the right tool in Java 17.

Solution 3

package com.nexussoftware.bibliotech.service;

public class Auditor {

    private String source = "Auditor";

    public void audit() {

        Runnable task = new Runnable() {

            private String source = "Task";

            @Override
            public void run() {
                System.out.println("1. source (no this)      = " + source);
                System.out.println("2. this.source           = " + this.source);
                System.out.println("3. Auditor.this.source   = " + Auditor.this.source);
                System.out.println("4. this.getClass()       = " + this.getClass().getName());
                System.out.println("5. Auditor.this.getClass = "
                                   + Auditor.this.getClass().getName());
            }
        };

        task.run();
    }

    public static void main(String[] args) {
        new Auditor().audit();
    }
}
1. source (no this)      = Task
2. this.source           = Task
3. Auditor.this.source   = Auditor
4. this.getClass()       = com.nexussoftware.bibliotech.service.Auditor$1
5. Auditor.this.getClass = com.nexussoftware.bibliotech.service.Auditor

Line-by-line analysis:

  1. source without a prefix returns "Task". Resolution goes from the inside out: there is no local variable with that name, so the anonymous class's field wins and Auditor's is shadowed. This is exactly the mechanism by which an innocent change — adding a field to the anonymous class — silently redirects accesses that used to go to the outer class.
  2. this.source is the same as line 1, and here it is explicit: this is the anonymous class.
  3. Auditor.this.source is the only way to reach the enclosing class's field.
  4. this.getClass() returns Auditor$1, the class the compiler generated (04-03, section 9). The 1 indicates it is the first anonymous class in Auditor; if you add another before it, this one becomes Auditor$2.
  5. Auditor.this.getClass() returns Auditor, confirming they are two different objects.

Repeat this same exercise after finishing 04-05, replacing the anonymous class with a lambda: all five lines change their result, because in a lambda this is the outer class. That difference is the number one source of errors when migrating old code from anonymous classes to lambdas.

Conclusion

You now know how to declare and instantiate an implementation in a single expression and without giving it a name. You have mastered the dissected syntax — the type being implemented or extended, the parentheses that go to the superclass constructor, the body in braces and that final semicolon which closes a statement, not a class — and you recognise }); as the visual signature of an anonymous class in any Java code you open.

You know its limits and the reason for each one: it cannot have a constructor because a constructor carries the name of its class and this one has none, and that is why the instance initialisation block exists; it can declare its own methods, but they are unreachable from outside because the variable has the interface's type and the actual type cannot be named; and it captures effectively final variables, with the same rule and the same reason as local classes. And you are very aware of the trap that costs the most: inside an anonymous class, this is the anonymous class, not the enclosing one; to reach the latter you have to write Outer.this.

You have seen the three uses that made it Java's workhorse for fifteen years: the Comparator that sorts the BiblioTech catalogue by title, by term or by fine; the callback that lets LoanManager notify without knowing whom, with two different listeners defined where the decision about what to do is made; and the one-off extension of a class, including an abstract one, which makes new Material(...) { ... } legal where new Material(...) is forbidden. You know why double brace initialization is bad practice — it changes the object's actual class and breaks equals, retains the outer instance and generates extra classes — and what each anonymous class costs in the bytecode: a numbered .class, cryptic traces and renumbering when you reorder.

And above all you take away the decision table: a top-level class when the type has substance, a static nested one for helpers, a local one for types belonging to a single algorithm, an anonymous one when you need state, several abstract methods or to extend a class, and a lambda when none of that is needed.

Because that last row is what comes next. Look back over the comparators you have written in this lesson: each has one line of logic wrapped in five of ceremony. The compiler already knows the interface's name, because the parameter of Arrays.sort says so. It knows the method's name too, because Comparator has only one. It knows the types of a and b too. All of that is redundant. In lesson 04-05, Lambda Expressions, you will strip the ceremony away piece by piece until only what carries information is left, you will learn the full syntax with all its shorthand forms, you will understand what a lambda really is — not a function pointer, but the implementation of a functional interface — you will see why this behaves the opposite way to here, and you will write your own BiblioTech functional interfaces to change a class's behaviour without touching its code.

Java Programming Course

Module 1: Introduction to Java

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File Input/Output

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2026. All rights reserved