Java Anonymous Classes
Java Anonymous Classes
An anonymous class is a class with no name - its declaration and its one instantiation happen in the same statement, written as new SomeType() immediately followed by a class body in braces. The moment that statement runs, Java creates a class on the spot, gives it whatever methods you wrote inside that body, and hands you back exactly one instance of it. No separate file, no class declaration anywhere else in the codebase - just an implementation that exists precisely where it is needed and nowhere else. This is the syntax behind every inline Comparator written before Java 8, every Swing button click handler, and it remains the only option today when a single-method interface is not enough.
What Is an Anonymous Class?
An anonymous class is created with the new keyword followed by either an interface name or a class name, followed immediately by a class body in braces - and a semicolon, since the whole thing is an expression that produces a value.
new SuperTypeOrInterface(constructorArgsIfAny) {
// override methods, optionally declare new fields and helper methods
};
TWO SHAPES:
IMPLEMENTING AN INTERFACE - no constructor arguments possible,
because interfaces have no constructors:
Runnable task = new Runnable() {
@Override
public void run() { ... }
};
EXTENDING A CLASS (including abstract classes) - constructor
arguments are passed to the superclass's constructor:
Greeter greeter = new Greeter("Priya") {
@Override
String greet() { ... }
};
Two restrictions follow directly from "no name." An anonymous class declaration can have at most one supertype - either it implements one interface, or it extends one class, never both, and never more than one interface. And it cannot declare its own constructor - there is nothing to give a name to, so when extending a class, the constructor arguments you supply are simply forwarded to the superclass's constructor.
Basic Overview - The Two Shapes and What They Capture
SHAPE 1 - IMPLEMENTING AN INTERFACE
Fresher view : fill in the blanks of an interface, written exactly
where you need that implementation
Deeper view : compiles to OuterClass dollar N implementing that
interface - if the interface has exactly ONE abstract
method, a lambda usually says the same thing shorter
SHAPE 2 - EXTENDING A CLASS (INCLUDING ABSTRACT CLASSES)
Fresher view : create a customized version of an existing class,
for one specific use, without a separate subclass file
Deeper view : the ONE thing a lambda categorically cannot do -
lambdas target functional interfaces only, never
classes or abstract classes
CAPTURING VARIABLES FROM THE ENCLOSING SCOPE
Fresher view : code inside the anonymous class can use variables
from the method around it, as if they were just there
Deeper view : captured BY VALUE, at construction time, and the
captured variable must be effectively final - never
reassigned after that point
DECLARING EXTRA MEMBERS
Fresher view : the anonymous class can have its own fields and
helper methods beyond what the interface or class
requires it to implement
Deeper view : those extra members exist only on the anonymous
class's actual (synthetic) type - code holding a
reference typed as the interface or superclass
cannot see them, only code inside the anonymous
class body itself can
A fresher mostly needs shape 1 to recognize the pattern in the wild - it is everywhere in code written before Java 8, and still common today. Shape 2, the capture rules, and the "extra members are invisible outside" point are where this topic earns its place in interviews - they are the parts that look obvious until a specific example makes the underlying rule click.
Why Anonymous Classes Matter
Before Java 8, anonymous classes were the only inline option for implementing an interface - every Comparator passed to a sort call, every ActionListener registered with a Swing button, every Runnable handed to a new Thread, was written this way. Lambda expressions now cover the most common case - a single-method interface implemented inline - more concisely. But anonymous classes did not become obsolete; they became the answer to two specific questions a lambda cannot answer.
The first question: does the type being implemented have more than one abstract method? A Comparator<T> has one (compare), so a lambda fits. An event-callback interface with onSuccess, onFailure, and onRetry has three - a lambda has no way to provide three method bodies, so an anonymous class is the only inline option.
The second question: are you extending a class - including an abstract class - rather than implementing an interface? Lambdas exist purely as syntax for functional interfaces; they cannot extend anything. A TimerTask subclass written inline, or a small customized version of an abstract base class created for one specific call, requires an anonymous class.
A third, quieter reason anonymous classes still matter: locality. When the implementation is short and used in exactly one place, writing it inline - right where it is passed - keeps the reader from having to jump to a separate file to understand what happens. During code reviews, the question is rarely "should this be anonymous or named" in isolation; it is "is this implementation simple enough, and used in few enough places, that inline is more readable than a name would be."
How Anonymous Classes Work
Declaring and Instantiating
The two shapes from the overview - implementing an interface, and extending a class - look different mainly in whether constructor arguments appear after the supertype name. The example below shows both: Runnable (an interface, no constructor) and Greeter (an abstract class with a constructor that takes a name).
1// File: AnonymousSyntaxDemo.java
2
3public class AnonymousSyntaxDemo {
4
5 // An abstract class WITH a constructor - anonymous classes extending
6 // a class can pass arguments to that constructor
7 static abstract class Greeter {
8 private final String name;
9
10 Greeter(String name) {
11 this.name = name;
12 }
13
14 abstract String greet();
15
16 String getName() { return name; }
17 }
18
19 public static void main(String[] args) {
20
21 System.out.println("=== Anonymous class implementing an interface ===");
22 // Runnable has ONE abstract method - run(). No constructor
23 // arguments are possible, because interfaces have no constructors.
24 Runnable task = new Runnable() {
25 @Override
26 public void run() {
27 System.out.println(" Running scheduled cleanup task");
28 }
29 };
30 task.run();
31
32 System.out.println();
33
34 System.out.println("=== Anonymous class extending an abstract class ===");
35 // "Priya" is forwarded to Greeter's constructor. getName() is
36 // inherited from Greeter and used inside the overridden greet().
37 Greeter greeter = new Greeter("Priya") {
38 @Override
39 String greet() {
40 return "Hello, " + getName() + "!";
41 }
42 };
43 System.out.println(" " + greeter.greet());
44 }
45}Output:
=== Anonymous class implementing an interface ===
Running scheduled cleanup task
=== Anonymous class extending an abstract class ===
Hello, Priya!
Capturing Variables From the Enclosing Scope
An anonymous class can read local variables and parameters of the method it is declared in - but, exactly like a local class, only if those variables are effectively final: assigned once, never reassigned afterward. The compiler copies the variable's value into the anonymous class instance at the moment it is constructed. What is often missed: this restriction applies to reassigning the variable itself, not to mutating an object the variable refers to - a captured List reference cannot be pointed at a different list, but items can still be added to it freely.
1// File: AnonymousCaptureDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class AnonymousCaptureDemo {
7
8 interface OrderProcessor {
9 void process(String orderId);
10 }
11
12 public static void main(String[] args) {
13
14 double discountPercent = 10.0; // effectively final - never reassigned
15 List<String> processedOrders = new ArrayList<>(); // the REFERENCE is effectively final
16
17 OrderProcessor processor = new OrderProcessor() {
18 @Override
19 public void process(String orderId) {
20 // Reading captured variables - allowed because both
21 // are effectively final
22 System.out.printf(" Processing %s with %.0f%% discount%n", orderId, discountPercent);
23
24 // Mutating the CONTENTS of a captured collection is fine -
25 // only reassigning 'processedOrders' itself would not be
26 processedOrders.add(orderId);
27 }
28 };
29
30 processor.process("ORD-001");
31 processor.process("ORD-002");
32
33 System.out.println("Processed orders: " + processedOrders);
34
35 // The following would NOT COMPILE if it appeared anywhere in
36 // this method, before or after the anonymous class above:
37 //
38 // discountPercent = 15.0;
39 // // COMPILE ERROR - "local variables referenced from an inner
40 // // class must be final or effectively final" - the anonymous
41 // // OrderProcessor above reads discountPercent, so it can
42 // // never be reassigned anywhere in this method
43 }
44}Output:
Processing ORD-001 with 10% discount
Processing ORD-002 with 10% discount
Processed orders: [ORD-001, ORD-002]
this Inside an Anonymous Class vs Inside a Lambda
This is the detail that most reliably separates developers who have used anonymous classes from those who understand them. Inside an anonymous class's methods, this refers to the anonymous class instance itself - if the anonymous class declares its own field with the same name as a field on the enclosing class, this.fieldName resolves to the anonymous class's own field, not the enclosing one. Inside a lambda, there is no new this at all - this continues to refer to whatever it referred to in the enclosing method, exactly as if the lambda's body were inline code in that method.
1// File: AnonymousVsLambdaThisDemo.java
2
3public class AnonymousVsLambdaThisDemo {
4
5 private String label = "EnclosingInstance";
6
7 void demonstrate() {
8
9 Runnable anonymous = new Runnable() {
10 private String label = "AnonymousInstance"; // shadows the outer 'label'
11
12 @Override
13 public void run() {
14 // 'this' here is THIS ANONYMOUS CLASS INSTANCE
15 System.out.println("Anonymous - this.label : " + this.label);
16 // Outer.this reaches the ENCLOSING instance explicitly
17 System.out.println("Anonymous - outer.label : " + AnonymousVsLambdaThisDemo.this.label);
18 }
19 };
20
21 Runnable lambda = () -> {
22 // 'this' inside a lambda is the SAME 'this' as the
23 // enclosing method - lambdas introduce no new 'this'
24 System.out.println("Lambda - this.label : " + this.label);
25 };
26
27 anonymous.run();
28 lambda.run();
29 }
30
31 public static void main(String[] args) {
32 new AnonymousVsLambdaThisDemo().demonstrate();
33 }
34}Output:
Anonymous - this.label : AnonymousInstance
Anonymous - outer.label : EnclosingInstance
Lambda - this.label : EnclosingInstance
Internal Working - Compiled Representation
Every anonymous class compiles to its own .class file, named after the enclosing class plus a sequential number - there is no other name to use.
SOURCE: AnonymousVsLambdaThisDemo.java contains ONE anonymous class
COMPILED OUTPUT:
AnonymousVsLambdaThisDemo.class
AnonymousVsLambdaThisDemo$1.class <- the anonymous Runnable
AnonymousVsLambdaThisDemo$1
+-----------------------------------+
| this$0 : AnonymousVsLambdaThisDemo | <- present because this anonymous
| label : String | class was declared inside an
+-----------------------------------+ INSTANCE method - it can reach
the enclosing instance
NUMBERING IS PER ENCLOSING CLASS, NOT PER METHOD:
Every anonymous class declared ANYWHERE inside
AnonymousVsLambdaThisDemo receives the next number in sequence -
$1, $2, $3 - regardless of which method it appears in. Two
anonymous classes in two different methods of the same enclosing
class do not restart the count.
CAPTURED VARIABLES BECOME SYNTHETIC FIELDS:
For AnonymousCaptureDemo's anonymous OrderProcessor, which captures
'discountPercent' and 'processedOrders':
AnonymousCaptureDemo$1
+------------------------------------+
| val$discountPercent : double | <- a COPY of the captured
| val$processedOrders : List<String> | value, stored as a field,
+------------------------------------+ set via a synthetic
constructor parameter
This is WHY reassigning a captured variable afterward is disallowed -
val$discountPercent is a snapshot taken once, at construction. If the
original could change later, this snapshot would silently go stale.
LAMBDAS USE A DIFFERENT MECHANISM:
A lambda for a functional interface generally does not produce a
named OuterClass dollar N class file at compile time at all. The
compiler emits an invokedynamic instruction, and the actual
implementing class is generated at runtime by LambdaMetafactory.
This is part of why a codebase with many lambdas does not accumulate
one numbered .class file per lambda the way anonymous classes do.
Real-World Example - Urban Company Service Booking
A home-services booking platform needs to notify a caller about three distinct outcomes of a booking request - a professional being assigned, the booking being confirmed with a time slot, or the booking being declined - and, on confirmation, schedule a reminder for that slot. The three-outcome callback cannot be a lambda (more than one abstract method), and the reminder is built by extending an abstract task class inline, capturing the specific slot it was created for.
1// File: BookingCallback.java
2
3public interface BookingCallback {
4 void onProfessionalAssigned(String professionalName);
5 void onConfirmed(String professionalName, String slot);
6 void onDeclined(String reason);
7}1// File: ReminderTask.java
2
3public abstract class ReminderTask {
4
5 private final String bookingId;
6
7 protected ReminderTask(String bookingId) {
8 this.bookingId = bookingId;
9 }
10
11 public String getBookingId() { return bookingId; }
12
13 public abstract void send();
14
15 public final void execute() {
16 System.out.println(" Executing reminder for booking: " + bookingId);
17 send();
18 }
19}1// File: ServiceBookingService.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class ServiceBookingService {
7
8 public void requestBooking(String serviceType, BookingCallback callback) {
9 if (serviceType.equals("AC_REPAIR")) {
10 callback.onProfessionalAssigned("Ramesh Kumar");
11 callback.onConfirmed("Ramesh Kumar", "Today, 4:00 PM - 6:00 PM");
12 } else if (serviceType.equals("UNAVAILABLE_SERVICE")) {
13 callback.onDeclined("No professionals available for: " + serviceType);
14 } else {
15 callback.onProfessionalAssigned("Priya Sharma");
16 callback.onConfirmed("Priya Sharma", "Tomorrow, 10:00 AM - 12:00 PM");
17 }
18 }
19
20 // Returns an anonymous class extending the abstract ReminderTask -
21 // 'slot' is captured here and used inside send() later
22 public ReminderTask createReminder(String bookingId, String slot) {
23 return new ReminderTask(bookingId) {
24 @Override
25 public void send() {
26 System.out.println(" Reminder: Your service is scheduled for " + slot);
27 }
28 };
29 }
30
31 public static void main(String[] args) {
32 ServiceBookingService service = new ServiceBookingService();
33 List<String> auditLog = new ArrayList<>();
34
35 // Anonymous class implementing a THREE-method interface -
36 // this cannot be written as a lambda
37 BookingCallback callback = new BookingCallback() {
38 @Override
39 public void onProfessionalAssigned(String professionalName) {
40 auditLog.add("ASSIGNED: " + professionalName);
41 System.out.println(" Professional assigned: " + professionalName);
42 }
43
44 @Override
45 public void onConfirmed(String professionalName, String slot) {
46 auditLog.add("CONFIRMED: " + professionalName + " at " + slot);
47 System.out.println(" Booking confirmed with " + professionalName + " for " + slot);
48
49 // 'service' is captured from the enclosing main() method -
50 // effectively final, never reassigned
51 ReminderTask reminder = service.createReminder("BOOK-1001", slot);
52 reminder.execute();
53 }
54
55 @Override
56 public void onDeclined(String reason) {
57 auditLog.add("DECLINED: " + reason);
58 System.out.println(" Booking declined: " + reason);
59 }
60 };
61
62 System.out.println("=== Successful booking with reminder ===");
63 service.requestBooking("AC_REPAIR", callback);
64
65 System.out.println();
66 System.out.println("=== Declined booking ===");
67 service.requestBooking("UNAVAILABLE_SERVICE", callback);
68
69 System.out.println();
70 System.out.println("=== Audit log ===");
71 auditLog.forEach(entry -> System.out.println(" " + entry));
72 }
73}Output:
=== Successful booking with reminder ===
Professional assigned: Ramesh Kumar
Booking confirmed with Ramesh Kumar for Today, 4:00 PM - 6:00 PM
Executing reminder for booking: BOOK-1001
Reminder: Your service is scheduled for Today, 4:00 PM - 6:00 PM
=== Declined booking ===
Booking declined: No professionals available for: UNAVAILABLE_SERVICE
=== Audit log ===
ASSIGNED: Ramesh Kumar
CONFIRMED: Ramesh Kumar at Today, 4:00 PM - 6:00 PM
DECLINED: No professionals available for: UNAVAILABLE_SERVICE
callback is one anonymous class with three method bodies, service and auditLog are captured into it from main, and createReminder returns a second, completely separate anonymous class - one extending ReminderTask, capturing slot - each time it is called. Both anonymous classes exist purely because of this one booking flow; neither would make sense as a standalone named type anywhere else in the codebase.
Anonymous Class vs Lambda vs Named Inner Class
| Aspect | Anonymous Class | Lambda Expression | Named Inner Class (static or member) |
|---|---|---|---|
| Can implement an interface with multiple abstract methods | Yes | No | Yes |
| Can extend a class or abstract class | Yes | No | Yes |
Has its own this | Yes - distinct from enclosing this | No - this is the enclosing instance's | Yes |
| Can declare its own fields beyond the interface/class | Yes | No (only captures, no fields) | Yes |
| Can declare its own constructor | No | No | Yes |
| Reusable by a type name elsewhere | No - exists at one new expression | No | Yes |
| Compiled representation | Outer$N.class, generated at compile time | Usually no dedicated .class file - generated at runtime via invokedynamic | Outer$Name.class, generated at compile time |
| Typical use | Multi-method callback, extending an abstract class inline, one-off with extra state | Single-method functional interface, inline | Reusable implementation, referenced by name in multiple places |
If a single row had to summarize the decision: count the abstract methods, and check whether you are implementing an interface or extending a class. One abstract method and an interface - lambda. More than one method, or extending a class - anonymous class. Needed in more than one place, or needs its own constructor - named class, nested or otherwise.
Best Practices
Reach for an anonymous class specifically when a lambda cannot apply - not as a default. If the target type is a functional interface with exactly one abstract method, a lambda almost always reads better. Anonymous classes earn their place when the interface has multiple methods, when extending a class or abstract class, or when the implementation needs its own fields beyond what captured variables provide - as BookingCallback's three-method implementation did above.
Keep anonymous class bodies short. An anonymous class that grows past a handful of lines, or accumulates multiple helper methods and fields of its own, is signaling that it has become a real, reusable type - at that point, a private static nested class with a real name is clearer to read, easier to test on its own, and avoids an ever-growing inline block at the call site.
Be deliberate about what gets captured, especially this. An anonymous class declared inside an instance method captures the enclosing this implicitly the moment it references any instance member - including indirectly, through a method call on this. If that anonymous instance is registered somewhere long-lived (a static listener registry, a cache), it keeps the entire enclosing object reachable for as long as the registration exists. This is the same this$0 retention concern that applies to non-static inner classes, and it applies to anonymous classes identically.
Remember the "one supertype" restriction when designing interfaces meant for inline implementation. An anonymous class can implement exactly one interface or extend exactly one class - never both, and never more than one interface. If a piece of inline logic genuinely needs to satisfy two different interfaces at once, anonymous classes cannot do it; a named class that implements both is the only option.
Common Mistakes
Mistake 1 - Reassigning a Captured Variable After the Anonymous Class Is Created
1// WRONG - 'counter' is reassigned AFTER the anonymous Runnable is
2// created and reads it - not effectively final, COMPILE ERROR
3void brokenCounter() {
4 int counter = 0;
5
6 Runnable incrementer = new Runnable() {
7 @Override
8 public void run() {
9 System.out.println(counter); // COMPILE ERROR
10 }
11 };
12
13 counter = 1; // this later reassignment is what breaks the capture
14}
15
16// CORRECT - if the value genuinely needs to change after the anonymous
17// class is created, capture a MUTABLE HOLDER instead of a primitive -
18// the holder REFERENCE stays effectively final, only its CONTENTS change
19void fixedCounter() {
20 int[] counter = {0};
21
22 Runnable incrementer = new Runnable() {
23 @Override
24 public void run() {
25 counter[0]++;
26 System.out.println(counter[0]);
27 }
28 };
29
30 incrementer.run();
31 incrementer.run();
32}Mistake 2 - Assuming a Field Reference Inside an Anonymous Class Reaches the Enclosing Field
1// WRONG ASSUMPTION - a developer might expect the println below to
2// print "EMAIL" (NotificationService's field), but the anonymous
3// class declares its OWN 'channel' field, which SHADOWS the outer one.
4// 'channel' inside run() resolves to the anonymous class's own field.
5public class NotificationService {
6 private String channel = "EMAIL";
7
8 Runnable buildTask() {
9 return new Runnable() {
10 private String channel = "SMS";
11
12 @Override
13 public void run() {
14 System.out.println("Sending via: " + channel); // prints "SMS"
15 }
16 };
17 }
18}
19
20// CORRECT - to reach the outer field explicitly, use Outer.this.fieldName
21public class NotificationServiceFixed {
22 private String channel = "EMAIL";
23
24 Runnable buildTask() {
25 return new Runnable() {
26 private String channel = "SMS";
27
28 @Override
29 public void run() {
30 System.out.println("Anonymous channel: " + channel); // "SMS"
31 System.out.println("Outer channel : " +
32 NotificationServiceFixed.this.channel); // "EMAIL"
33 }
34 };
35 }
36}Mistake 3 - Writing an Anonymous Class for a Single-Method Interface Where a Lambda Says the Same Thing
1// WRONG - not a compile error, but unnecessarily verbose for an
2// interface with exactly ONE abstract method
3products.sort(new java.util.Comparator<String>() {
4 @Override
5 public int compare(String first, String second) {
6 return first.compareTo(second);
7 }
8});
9
10// CORRECT - Comparator<String> is a functional interface - a lambda
11// expresses the same comparison with far less ceremony
12products.sort((first, second) -> first.compareTo(second));
13
14// Or, when the lambda body is itself just one method call, a method
15// reference is shorter still
16products.sort(String::compareTo);Mistake 4 - An Anonymous Class Capturing the Enclosing Instance Through a Long-Lived Registration
1interface RefreshListener {
2 void onRefresh();
3}
4
5class GlobalEventBus {
6 private static final java.util.List<RefreshListener> listeners = new java.util.ArrayList<>();
7
8 static void addListener(RefreshListener listener) {
9 listeners.add(listener);
10 }
11
12 static void removeListener(RefreshListener listener) {
13 listeners.remove(listener);
14 }
15}
16
17// WRONG - this anonymous class is declared inside an INSTANCE method,
18// so referencing 'dashboardId' (an instance field) captures 'this' of
19// ReportDashboard implicitly. Registering it with a STATIC, application
20// -wide bus keeps this ENTIRE ReportDashboard instance reachable for as
21// long as GlobalEventBus holds the listener - even after the dashboard
22// should have been discarded.
23class ReportDashboard {
24 private final String dashboardId;
25
26 ReportDashboard(String dashboardId) {
27 this.dashboardId = dashboardId;
28 }
29
30 void registerRefreshListener() {
31 GlobalEventBus.addListener(new RefreshListener() {
32 @Override
33 public void onRefresh() {
34 System.out.println("Refreshing dashboard: " + dashboardId);
35 }
36 });
37 }
38}
39
40// CORRECT - keep a reference to the listener and remove it when the
41// dashboard is no longer needed, breaking the retention path
42class ReportDashboardFixed {
43 private final String dashboardId;
44 private RefreshListener listener;
45
46 ReportDashboardFixed(String dashboardId) {
47 this.dashboardId = dashboardId;
48 }
49
50 void registerRefreshListener() {
51 listener = new RefreshListener() {
52 @Override
53 public void onRefresh() {
54 System.out.println("Refreshing dashboard: " + dashboardId);
55 }
56 };
57 GlobalEventBus.addListener(listener);
58 }
59
60 void close() {
61 GlobalEventBus.removeListener(listener); // releases the retained reference
62 }
63}Interview Questions
Q1. What is an anonymous class in Java, and how is it different from a named inner class?
An anonymous class is a class with no name, declared and instantiated in a single new SuperType() expression with an attached class body - the class and its one instance are created together, at that exact point in the code. A named inner class (member, static nested, or local) has a declared name, can be instantiated multiple times from multiple places using that name, and can declare its own constructors. Anonymous classes cannot do either - there is exactly one instance per new expression, and any constructor arguments are forwarded directly to the supertype's constructor rather than to a constructor of the anonymous class itself, because the anonymous class has no name to attach a constructor to.
Q2. Can an anonymous class have its own constructor?
No. A constructor is named after its class, and an anonymous class has no name to give one. When an anonymous class extends a class with constructors, the arguments written after the supertype name in new SuperType(args) are passed to that superclass's constructor - they configure the part of the object inherited from SuperType, not some constructor belonging to the anonymous class itself. Any additional setup the anonymous class needs beyond what the superclass constructor provides has to happen through an instance initializer block - a bare brace-delimited block inside the class body - or be deferred to the first method call.
Q3. What determines whether code creating an anonymous class can instead be written as a lambda?
Two conditions, both required: the supertype must be an interface (not a class or abstract class), and that interface must be a functional interface - exactly one abstract method. If either condition fails - the supertype is a class, or the interface has more than one abstract method - a lambda cannot express the same thing, and an anonymous class (or a named class) is required. This is why Runnable, Comparator<T>, and Callable<V> are commonly written as lambdas, while a multi-method callback interface or any abstract class subclass is not.
Q4. Inside an anonymous class, what does this refer to, and how does that differ from inside a lambda?
Inside an anonymous class's methods, this refers to the anonymous class instance itself - if the anonymous class declares a field with the same name as a field on the enclosing class, this.fieldName resolves to the anonymous class's own field, shadowing the enclosing one. To reach the enclosing instance explicitly, EnclosingClassName.this.fieldName is required. Inside a lambda, there is no new this - the lambda body behaves as if it were inline code in the enclosing method, so this continues to refer to whatever it referred to there. This difference is a common source of subtle bugs when code is converted from an anonymous class to a lambda (or vice versa) without accounting for what this resolves to in each form.
Q5. What is the rule for capturing local variables in an anonymous class, and why does it exist?
A local variable or parameter from the enclosing method can be read inside an anonymous class only if it is effectively final - assigned exactly once, never reassigned afterward, even if not declared final explicitly. The reason is that the anonymous class instance may outlive the method call that created it - if it is returned, stored, or handed to another thread - while the enclosing method's stack frame (where the original variable lives) will not. The compiler copies the variable's value into the anonymous class instance, as a synthetic field, at construction time. If the original variable could change after that point, the copy would silently become stale and inconsistent, so Java disallows the possibility at compile time rather than letting it happen at runtime.
Q6. How are anonymous classes named in compiled bytecode, and does the numbering reset per method?
An anonymous class compiles to EnclosingClass$N.class, where N is a sequential number. The numbering is scoped to the entire enclosing class, not to individual methods - the first anonymous class declared anywhere inside EnclosingClass, regardless of which method it is in, becomes $1; the second, wherever it appears, becomes $2; and so on. Two anonymous classes in two different methods of the same enclosing class do not each start their own numbering from $1. This is why stack traces and decompiled code show numbers rather than method-relative identifiers when anonymous classes are involved.
FAQs
Can an anonymous class implement more than one interface?
No. An anonymous class declaration can have at most one supertype written after new - either one interface it implements, or one class (including abstract classes) it extends. It cannot implement two interfaces, and cannot both extend a class and implement an interface at the same time. A named class does not have this restriction - class Foo implements A, B is fine - but an anonymous class's single-supertype limit is absolute.
Can an anonymous class be declared static?
The static keyword is not written for anonymous classes, but the same idea applies based on context: if an anonymous class is declared inside a static method or a static initializer block, it has no enclosing instance to reference - there is no this$0, exactly as if it were a static nested class. If it is declared inside an instance method, it can capture the enclosing instance, exactly as a non-static inner class would.
Can you create more than one instance of the same anonymous class?
Not from the same new expression - by definition, that expression both declares the class and creates its one instance, in that single statement. If the same anonymous-class code is written again at a different location, the compiler treats it as a separate anonymous class with its own number ($1, $2, and so on), even if the two bodies are textually identical - they are not the same class. If multiple instances of one implementation are genuinely needed, a named class (which can be instantiated with new as many times as required) is the appropriate tool.
Can an anonymous class's overridden method throw a checked exception that the original method does not declare?
No - the standard overriding rules apply. If the interface method or abstract method being implemented does not declare a checked exception in its throws clause, the anonymous class's implementation cannot introduce one either. This is the same Liskov-substitution constraint that applies to any override, anonymous or otherwise - code calling through the declared interface or supertype must not be surprised by a checked exception the type's contract never promised.
Can an anonymous class have its own type parameters, like a generic named class?
No. Anonymous classes cannot declare type parameters of their own - writing new Comparator<T>() with its own body is not valid if T is meant to be a fresh type parameter introduced by the anonymous class itself. An anonymous class CAN implement a parameterized interface with a concrete type argument, such as new Comparator<String>(), where String is a real, already-known type - it simply cannot introduce a new, independent type variable the way a named class (including a static nested class) can.
Why does my IDE or stack trace show a class name like "ServiceBookingService dollar 1" instead of something descriptive?
That is the compiled name of an anonymous class - EnclosingClass$N, where N is its sequential number within the enclosing class. The anonymous class has no source-level name, so the compiler generates this one. If a particular anonymous class shows up often enough in stack traces or debugging sessions that the numbered name becomes a hindrance, that is usually a sign worth treating as a hint: giving that implementation a real name, as a small private static nested class, would make future debugging of that code path noticeably easier.
Summary
An anonymous class packs a declaration and a single instantiation into one expression - new SuperType() plus a body in braces - at the exact point an implementation is needed, with no name and no separate file. It can implement one interface or extend one class, never both and never more than one interface, and it cannot declare its own constructor.
Two facts separate confident usage from surface-level familiarity. First, this inside an anonymous class refers to the anonymous class instance itself - distinct from the enclosing instance, reachable only via EnclosingClass.this - while a lambda introduces no new this at all. Second, captured local variables are copied by value at construction time and must be effectively final, which is why reassigning a variable an anonymous class reads is a compile error rather than a subtle runtime bug.
The practical decision this topic comes down to: count the abstract methods on the type you are implementing, and check whether it is an interface or a class. One method, an interface - a lambda says it shorter. More than one method, or a class to extend, or state of its own beyond what capture provides - an anonymous class remains exactly the right tool, the same tool it has been since before lambdas existed.
What to Read Next
Learn how to define a fixed set of constant values.