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
- What an anonymous class is
- The syntax, dissected
- What it can and cannot do
- The instance initialisation block
- Its own methods: they exist, but they are almost useless
- Capturing effectively final variables
thisinside an anonymous class- Classic use 1: a
Comparatorfor the catalogue - Classic use 2: a callback
- Classic use 3: extending a class on the spot
- When it is still the right option and when to use a lambda
- "Double brace initialization" and why to avoid it
- Bytecode weight and readability
- Final decision table
- Common Mistakes and Tips
- Exercises
- 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:
- It declares a new class implementing
Comparator<Material>. - It instantiates it.
- It returns that instance as the value of the expression.
- 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, semicolonThat }); is the visual signature of an anonymous class in Java code. Learn to recognise it.
- 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.
- 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.
- 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 COMPILEerror: 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")
); // 14It 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.
- 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:
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.
this inside an anonymous class
this inside an anonymous classThis 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 |
- Classic use 1: a
Comparator for the catalogue
Comparator for the catalogueThe 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.
- 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.
- 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));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.
- 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.
- "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:
- It creates an extra class per use. Each
{{ }}generates one more.classfile. A method called a thousand times with this syntax in a loop generates one class, yes, but a thousand instances of an unnecessary subclass. - The object is no longer of the class you think.
marta.getClass()returnsApp$1, notEmployee. And since well-writtenequalsmethods compare withgetClass()(03-09),marta.equals(otherEmployee)returnsfalseeven if they have the same identifier. It is a real bug and a hard one to find. - It retains the outer instance. It is a non-static inner class: it drags along the hidden
this$0reference with everything that entails (04-03), including the memory leak if the object is stored in a cache. - 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);
- 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
.jarand 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.
- 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:
sourcewithout 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 andAuditor'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.this.sourceis the same as line 1, and here it is explicit:thisis the anonymous class.Auditor.this.sourceis the only way to reach the enclosing class's field.this.getClass()returnsAuditor$1, the class the compiler generated (04-03, section 9). The1indicates it is the first anonymous class inAuditor; if you add another before it, this one becomesAuditor$2.Auditor.this.getClass()returnsAuditor, 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
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
