Java Inner, Static Nested, and Anonymous Classes
Learn which nested classes need an outer object, how short anonymous implementations work, and when a named class makes the code easier to understand. Follow one course-board example from simple helpers to a tested refactor.
Before you start: review interfaces, constructors and this, and scope. Each runnable example is a complete Java 21 Main.java program with no input required. The smaller early examples focus on syntax; the final model adds explicit validation.
What you will be able to do
- Distinguish a static nested class from a non-static member inner class.
- Construct an inner object with the correct enclosing instance.
- Use Outer.this to distinguish enclosing state from shadowing fields.
- Write local and anonymous implementations of an existing interface.
- Explain effectively final capture without confusing it with object immutability.
- Refactor a growing callback into a named implementation and verify unchanged behavior.
First ask: where is the class declared, and what does it need?
A course board has a title. A tiny label helper might belong beside the board code, while a live view might need to read one particular board's current title. Both can be declared inside CourseBoard, but that placement alone does not tell you whether they need an enclosing board object.
Nested is the umbrella term. In this lesson, a static nested class is a member declared with static, a member inner class is declared without static, a local class is declared inside a block, and an anonymous class is defined inside a new expression without its own declared name.
These are ways to organize ordinary Java objects, not separate execution engines. Each object can still have its own fields and methods. We will build plain text previews in memory: no UI framework, event loop, database, course-progress storage, or external service is connected.
Static nested: group a helper without attaching an outer instance
CourseBoard.Label is a type name. new CourseBoard.Label creates a Label object without first creating a CourseBoard. The static modifier belongs to the nested class declaration; it does not make every Label the same object or turn its instance fields into shared static fields.
The first example is intentionally small: Label stores text passed to its constructor. It has no automatic board title to read. To use a board instance from a static nested class, provide a reference explicitly; private access rules and the existence of an actual receiver are separate questions.
Use a static nested helper when the grouping helps readers but an implicit connection to one outer object would be unnecessary. If the helper becomes useful across unrelated parts of the application, a separate named top-level class may be a clearer home.
Main.java · A static nested helper needs no outer object
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Java basics
Member inner: create a view for one specific enclosing object
View is a non-static member class. java.new View() creates a view associated with the java board, and sql.new View() creates one associated with the sql board. The declared type is CourseBoard.View in both cases, but the enclosing instances are different.
Read java.new View() as: use this existing java object as the enclosing board, then construct its View. A static main method has no automatic CourseBoard instance, so new CourseBoard.View() is not a valid replacement for this member-inner construction.
The view can access its enclosing board's private title. Nesting does not make View a subclass of CourseBoard: there is no extends relationship here. It is a distinct object associated with another object, not a copied board and not a hidden second superclass.
Main.java · Each inner view belongs to a particular board
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Live: Java
Live: SQL
Read the construction in three steps
CourseBoard java = new CourseBoard("Java")constructs the outer board.java.new View()constructs a distinct View associated with that board.javaView.line()reads the title from that enclosing board, not from the separate SQL board.
Try making a second view from java. Both views should read Java because both use the same enclosing board. The views are still two separate objects.
When names overlap, say which object you mean
Inside View.line, this refers to the View object. CourseBoard.this refers to its enclosing CourseBoard object. In the shadowing example both objects declare a field named title, so the two expressions intentionally produce different text.
An unqualified title would resolve to the nearer View field in that example. The qualified expression is not a static field access and does not choose an arbitrary CourseBoard instance. It follows the enclosing-instance relationship established when this View was constructed.
Prefer clear names when you control the design; qualification is useful when ambiguity genuinely needs explaining. An anonymous class also has its own this, unlike a lambda expression. Lambda syntax and its different this behavior are outside this course; focus here on the anonymous object and its enclosing instance.
Main.java · Distinguish this from CourseBoard.this
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Preview panel / Java
Local class: give a method-scoped helper a name
LocalFormatter is declared inside main, before it is instantiated. Its name is available in that block after the declaration, rather than becoming a member type such as Main.LocalFormatter. A local class is useful when a short named implementation belongs to one operation.
The local implementation reads prefix from the surrounding method. Because prefix is initialized and never reassigned, it is effectively final. You do not have to write final for this example to compile, but assigning a different prefix later would invalidate the capture.
Scope and lifetime are different ideas. The local class name is limited to its block, but an object created there can be returned or stored under an accessible interface type and used later. Leaving a method does not automatically destroy every object created inside it.
Main.java · A named helper inside one method
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Review: Java
Anonymous class: declare and create a small implementation together
new Formatter() followed by a class body creates an instance of an unnamed class implementing Formatter. It does not directly instantiate the interface. The braces supply the required public format method, and the semicolon ends the variable declaration containing the whole expression.
This is a common callback shape: one piece of code receives an object and calls its agreed method. Our example invokes format directly; later CourseBoard.render invokes it on the caller's behalf. Merely creating the callback does not call its format method or start a background task.
An anonymous class has no declared name and cannot declare a named constructor. It can have fields, methods, and initialization code. The type after new is the interface implemented or class extended; a body after new SomeClass(arguments) instead creates an anonymous subclass and can pass superclass-constructor arguments.
Main.java · Create an interface implementation at the use site
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Review: Java
Separate creation from invocation
new Formatter() { ... }creates an implementing object.- The object captures the unchanged prefix local used by its method.
- Assigning it to formatter does not execute format.
- The later
formatter.format("Java")call executes the method and returns its String.
No listener is registered and no event happens in the background. A callback is only an object supplied for another piece of code to call.
Effectively final capture does not make referenced objects immutable
In the counter example, the local calls variable keeps the same array reference, so it is effectively final. Updating calls[0] changes the array element, not the local reference. The callback and main observe the same mutable array rather than independent frozen copies.
Compare calls[0]++ with calls = new int[]{0}. The first changes the shared object's contents; the second reassigns the captured local and would fail compilation. Ordinary enclosing instance fields can also change; they are not subject to the same effectively final local-variable rule.
This counter is a deliberate side-effect demonstration, not the design for our lab's pure formatter. Capturing mutable state can make ordering, tests, and concurrency harder to reason about. Do not use a one-element array merely to evade a capture error when an explicit state-owning object would be clearer.
Main.java · A captured reference is not a frozen object
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
1: Java
2: SQL
Calls: 2
Refactor when an anonymous body starts needing a name
The anonymous formatter and BannerFormatter both return Review: followed by the board title. The named version stores its prefix in a private final field supplied through a constructor. The board still receives Formatter, so its render method needs no knowledge of either implementation's class name.
This refactor moves an implicit captured dependency into an explicit constructor argument. A named class gives the behavior a searchable name and a natural place for constructor validation and focused tests. It can be top-level or static nested depending on where it is useful.
Compare outputs before and after the refactor instead of assuming that moving code preserves behavior. Here valid prefixes are stripped at construction; the comparison uses the same already-normalized Review prefix for both forms. For wider inputs, define matching validation policies explicitly rather than hiding a semantic change inside a structural refactor.
Main.java · Refactor the same behavior into a named class
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Review: Java
Review: Java
Same text: true
A live enclosing reference and a snapshot answer different questions
The full CourseBoard validates and strips its title. A View reads CourseBoard.this.title each time line runs, so renaming the same board changes the next live preview. A Label created by snapshot stores the title String available when that Label was constructed.
The saved label remains Java basics after the board becomes Java objects. That stability comes from storing immutable String text in a private final field, not from the static keyword alone. A static nested class could still hold mutable fields or explicit references if its design allowed them.
Holding an inner view can keep its enclosing object reachable for as long as that relationship is needed. That is not automatically a memory leak, but retaining short-lived owners through long-lived callbacks or views can be undesirable. Choose a detached value when only historical text is needed, and avoid promising when garbage collection will run.
Main.java · Compare a live inner view with a static snapshot
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Live: Java objects
Saved: Java basics
Why do the two lines disagree after rename?
- snapshot constructs a Label holding the current immutable title text.
- rename assigns a new validated title to the board.
- The existing View reads the current title from its enclosing object.
- The Label returns its own saved text, so it still reports the earlier title.
Static nesting does not itself guarantee immutability. The Label is stable because of its private final String and lack of mutation operations.
Choose the smallest form that makes ownership and behavior clear
Use a static nested class for a closely related helper without an automatic outer instance; use a member inner class when that enclosing-instance connection is genuinely useful. Use a local named class for block-scoped behavior and an anonymous implementation for a short use-site customization.
Use a separate named class when the behavior grows, is reused, needs substantial validation, or is easier to explain independently. Nested code is not inherently more encapsulated or better designed: visibility, responsibilities, dependencies, and tests still need deliberate choices.
Verify two boards stay independent, a live view follows only its own board, snapshots stay unchanged, and anonymous and named callbacks agree for the inputs they both promise to handle. Also test intended compiler failures: missing enclosing instances, reassigned captured locals, and non-public interface implementations.
Repair exercise: a captured prefix is reassigned
This anonymous class captures prefix, but the surrounding method later reassigns it. The starter must fail compilation. Decide which prefix the callback should use, initialize that value once before creating the callback, and preserve the interface implementation.
Main.java · Diagnose effectively final capture
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
This starter intentionally fails compilation. Expect a diagnostic stating that local variables referenced from an inner class must be final or effectively final. There is no expected runtime output until it is repaired.
Reveal the explained repair
The repaired callback captures a local initialized once to Practice. It no longer depends on a reassigned local. Adding final while keeping the later assignment would create another compiler error; using a mutable holder would introduce different semantics rather than explaining the intended fixed configuration.
Main.java · Capture the intended fixed prefix
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Practice: Java
Independent lab: live views, snapshots, and interchangeable formatters
Build an in-memory course-board preview. Keep a live view attached to its board, create a detached label snapshot, and compare an anonymous Formatter with a named BannerFormatter. Only main prints output; the model does not save course data, render a real UI, or schedule callbacks.
- Declare Formatter with String format(String title). The board supplies its valid normalized title, and the lab's implementations return formatted text without mutation or printing.
- Implement CourseBoard with private title, a constructor calling rename, and rename that validates before assigning. Null or blank titles throw IllegalArgumentException with Title must not be blank.; accepted titles are stripped.
- Declare public member inner View with line returning Live: followed by CourseBoard.this.title. Declare public static nested Label with private final text, a validating constructor, and text(). Its invalid-input message is Label must not be blank. Implement snapshot() using the current title.
- Implement render(Formatter) by rejecting null with Formatter must not be null. and delegating format(title). Implement BannerFormatter separately with a normalized private final prefix; invalid prefixes throw Prefix must not be blank. Share a requireText helper instead of duplicating inconsistent validation.
- In main create a board from padded Java basics, construct its View and snapshot, then rename to padded Java objects. Create an anonymous formatter capturing the effectively final Review prefix and a named formatter using the same prefix. Compare both through render.
- Print the supplied output, then demonstrate rejected blank-title and null-formatter cases. Check that a rejected rename preserves the last valid title and that another board never changes this board's view.
Before writing code, identify which object owns each piece of state. The completed program should produce:
Live: Java objects
Saved: Java basics
Review: Java objects
Review: Java objects
Same text: true
Rejected title: Title must not be blank.
Rejected formatter: Formatter must not be null.
Live: Java objects
Main.java · Build the course-board preview
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Stage 1 hints: define ownership and validation
CourseBoard owns the current title. Validate the incoming value before assigning, so a failed rename leaves state unchanged. Label owns a saved String, not a board reference. A static requireText helper can validate both without needing a board object.
Stage 2 hints: distinguish live and saved reads
Make View non-static and read CourseBoard.this.title in line. Construct it with board.new View(). Make Label static and pass the current title explicitly through snapshot. Test a rename before adding formatters.
Stage 3 hints: refactor without changing the caller
Create the anonymous Formatter with a prefix assigned once. Give BannerFormatter a constructor storing the equivalent normalized prefix. Pass both to render, which should only validate the formatter reference and delegate. Compare outputs through that same caller.
Reveal the complete explained solution
Main.java · Complete nested-class preview lab
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
View reads its enclosing board on every call, while Label retains previously copied immutable text. Both Formatter objects are called through the same interface. The anonymous implementation captures an unchanged local; the named implementation stores explicit constructor configuration. Validation happens before rename assigns, so rejection does not damage the last valid title.
Test changes and boundaries
- Create two boards and views. Rename only one and verify each view follows its own enclosing instance. Multiple views of one board should all see its current title.
- Save labels before and after a rename; each must retain its construction-time text. Construct a standalone new CourseBoard.Label without an outer board and show that separate labels have independent contents.
- Try null, empty, spaces, and tab/newline titles, labels, and prefixes. Verify exact errors, normalization of accepted text, and unchanged board state after a rejected rename. Reject null formatters before invocation.
- Compare anonymous and named formatters through render across several valid board titles and repeated calls. Confirm no title mutation and no hidden callback invocation merely from constructing the formatter.
- Deliberately reassign a captured local, omit the enclosing object when constructing View, and reduce format's visibility. Read the compiler diagnostics and repair the actual cause; do not suppress errors by deleting the contract.
Common mistakes and how to diagnose them
Using new CourseBoard.View() in static main
A member inner View needs an enclosing board. Construct board.new View(), or make a helper static only if its design truly needs no automatic outer instance.
Reading an outer instance field from a static nested class without a receiver
Static nesting provides no implicit CourseBoard.this. Supply a board explicitly or pass just the value required; do not turn unrelated instance fields static merely to silence the compiler.
Reassigning a captured local
A referenced surrounding local must be final or effectively final. Initialize the intended configuration once, or redesign ownership explicitly if changing state is genuinely required.
Confusing this with the enclosing object
Inside an inner or anonymous class, this is that inner or anonymous object. Use the correct Outer.this where available to identify the enclosing instance. Nesting is not inheritance.
Forgetting public, @Override, or the closing semicolon
An anonymous interface implementation still needs the correct public method signature. Its class body closes with }, and the containing variable declaration ends with a semicolon: };.
Using anonymous bodies for growing reusable policies
Move substantial behavior into a named class with explicit configuration and focused tests. Preserve validation and outputs during the refactor rather than merely moving syntax.
Frequently asked questions
Is every nested class an inner class?
No. Static nested classes do not have an automatic enclosing instance. Non-static member classes are inner classes; ordinary local and anonymous classes can also be inner forms. This lesson distinguishes placement from the need for an enclosing object.
Does static nested mean one shared object?
No. Each new CourseBoard.Label creates a separate Label instance. static removes the automatic outer-instance requirement; it does not make all fields or objects shared.
Can an anonymous class implement an interface?
Yes. new Formatter() with a body creates an unnamed implementing class and its instance. Without that implementing body, directly instantiating the interface would be illegal.
Can an anonymous class have a constructor?
It cannot declare a named constructor because it has no declared class name. It can initialize fields and use instance initializers; an anonymous subclass can also pass arguments to its superclass constructor.
Must captured locals explicitly say final?
No. Effectively final is sufficient: the local is assigned in an allowed initialization pattern and not subsequently reassigned. A final or effectively final reference may still point to mutable data.
Are inner classes forbidden from declaring static members?
Not in this Java 21 baseline. Restrictions from older Java tutorials changed in Java 16. The useful distinction here is whether an object has an enclosing-instance relationship, not an outdated blanket prohibition on static members.
Group code deliberately, and make dependencies clear
Static nested helpers need no automatic outer instance. Member inner objects are associated with an enclosing object. Local and anonymous classes can capture unchanged surrounding locals, but that does not freeze mutable referenced data. A named implementation makes growing behavior easier to configure, find, and test.
Explain why a live View changes after rename but a saved Label does not. Then explain why the anonymous formatter and BannerFormatter can be passed to the same render method. Next: enums, where a type represents a fixed set of supported values.
Can you identify the enclosing object and captured state?
Answer six questions and read the explanations. Quiz answers are temporary and do not save course progress.
References and related learning
The course-board examples, explanations, and exercises are original SovranCode material. These official Java 21 references document the relevant language rules; older tutorials may describe restrictions that no longer apply.
- Java 21 specification: inner classes and enclosing instances
- Java 21 specification: local class declarations
- Java 21 specification: anonymous class declarations
Review interfaces · Review scope · Java course roadmap.
Last updated: