Java · lesson 60 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Inner and Anonymous Classes in Java
Master nested classes, local classes, and anonymous classes — when to use them for event handlers, callbacks, and encapsulating helper logic.
By SkillByExample · Updated
Java lets you declare a class inside another class, or even inside a method. There are four
kinds: static nested, inner (non-static), local and anonymous. They differ in one
thing that matters: whether they carry a reference to an enclosing object. Each example is a
single file run with the JDK 21 source launcher (java File.java).
Static nested classes
A static nested class is an ordinary class that lives in another class’s namespace. It has no outer instance, so it can’t touch the outer class’s instance fields.
public class VehicleDemo {
public static void main(String[] args) {
Vehicle.Registration reg = new Vehicle.Registration("ABC-123");
reg.print();
}
}
class Vehicle {
private String make = "Toyota";
static class Registration {
private final String number;
Registration(String number) { this.number = number; }
void print() {
System.out.println("Reg: " + number);
System.out.println("Make: " + make);
}
}
}
$ java VehicleDemo.java
VehicleDemo.java:16: error: non-static variable make cannot be referenced from a static context
System.out.println("Make: " + make);
^
1 error
error: compilation failed
new Vehicle.Registration(...) needs no Vehicle, which is the point — and also why make,
which belongs to some particular Vehicle, isn’t reachable. Remove that line and it prints
Reg: ABC-123. Map.Entry is a static nested interface in the JDK.
Inner classes
A non-static nested class is created from an outer instance and can use its fields directly.
public class AccountDemo {
public static void main(String[] args) {
BankAccount account = new BankAccount(100);
BankAccount.Transaction tx = account.new Transaction();
tx.deposit(50);
BankAccount.Transaction tx2 = account.new Transaction();
tx2.deposit(25);
System.out.println("balance seen by account: " + account.balance());
}
}
class BankAccount {
private double balance;
BankAccount(double balance) { this.balance = balance; }
double balance() { return balance; }
class Transaction {
void deposit(double amount) {
balance += amount; // the outer object's private field
System.out.println("Deposited " + amount + ", balance " + balance);
}
}
}
$ java AccountDemo.java
Deposited 50.0, balance 150.0
Deposited 25.0, balance 175.0
balance seen by account: 175.0
account.new Transaction() ties each transaction to account, so both write to the same private
balance. Inside Transaction, balance is shorthand for BankAccount.this.balance.
Shadowing and Outer.this
When an inner class declares a name that the outer class also uses, the nearest one wins.
Outer.this reaches past it.
public class Shadowing {
private String name = "outer field";
class Inner {
private String name = "inner field";
void print(String name) {
System.out.println(name);
System.out.println(this.name);
System.out.println(Shadowing.this.name);
}
}
public static void main(String[] args) {
new Shadowing().new Inner().print("parameter");
}
}
$ java Shadowing.java
parameter
inner field
outer field
Three variables called name, three ways to reach them: the parameter, the inner object’s field
via this, and the outer object’s field via Shadowing.this.
Local classes
A local class is declared inside a method and is visible only there. It can read the method’s local variables if they are effectively final.
import java.util.ArrayList;
import java.util.List;
public class LocalClass {
static List<String> validate(List<String> emails, int maxLength) {
class Check {
final String email;
Check(String email) { this.email = email; }
String problem() {
if (!email.contains("@")) return "missing @";
if (email.length() > maxLength) return "longer than " + maxLength;
return null;
}
}
List<String> problems = new ArrayList<>();
for (String e : emails) {
String p = new Check(e).problem();
if (p != null) problems.add(e + ": " + p);
}
return problems;
}
public static void main(String[] args) {
System.out.println(validate(List.of("a@b.io", "nope", "very.long.name@example.com"), 15));
}
}
$ java LocalClass.java
[nope: missing @, very.long.name@example.com: longer than 15]
Check reads maxLength, a parameter of the enclosing method. A local class keeps a helper
right next to its only use instead of cluttering the outer class.
Anonymous classes
An anonymous class declares and instantiates a one-off implementation in a single expression. Unlike a lambda, it can implement an interface with several methods and hold its own state.
public class Anonymous {
interface KeyHandler {
void onPress(char key);
void onRelease(char key);
}
public static void main(String[] args) {
KeyHandler handler = new KeyHandler() {
private int presses = 0; // state of its own
public void onPress(char key) { presses++; System.out.println("pressed " + key + " (" + presses + ")"); }
public void onRelease(char key) { System.out.println("released " + key); }
};
handler.onPress('A');
handler.onRelease('A');
handler.onPress('B');
System.out.println(handler.getClass().getName());
}
}
$ java Anonymous.java
pressed A (1)
released A
pressed B (2)
Anonymous$1
KeyHandler has two abstract methods, so no lambda could implement it. The compiler names the
class Anonymous$1 — the first anonymous class inside Anonymous.
Captured variables must be effectively final
Anonymous classes, local classes and lambdas copy the local variables they use. If the variable could change afterwards, the copy would be stale, so the compiler forbids it.
public class Capture {
public static void main(String[] args) {
int clicks = 0;
Runnable r = new Runnable() {
public void run() { System.out.println("clicks = " + clicks); }
};
clicks++;
r.run();
}
}
$ java Capture.java
Capture.java:5: error: local variables referenced from an inner class must be final or effectively final
public void run() { System.out.println("clicks = " + clicks); }
^
1 error
error: compilation failed
The clicks++ after the class makes clicks not effectively final. To share a changing count,
keep it in an object — an int[] cell, an AtomicInteger, or a field — whose reference
never changes:
import java.util.concurrent.atomic.AtomicInteger;
public class CaptureFixed {
public static void main(String[] args) {
AtomicInteger clicks = new AtomicInteger();
Runnable click = () -> System.out.println("clicks = " + clicks.incrementAndGet());
click.run();
click.run();
}
}
$ java CaptureFixed.java
clicks = 1
clicks = 2
this in an anonymous class vs a lambda
public class ThisDemo {
private final String name = "ThisDemo instance";
void run() {
Runnable anon = new Runnable() {
public void run() { System.out.println("anonymous: " + this); }
public String toString() { return "the anonymous object"; }
};
Runnable lambda = () -> System.out.println("lambda: " + this);
anon.run();
lambda.run();
}
public String toString() { return name; }
public static void main(String[] args) { new ThisDemo().run(); }
}
$ java ThisDemo.java
anonymous: the anonymous object
lambda: ThisDemo instance
An anonymous class is a new object, so this is that object. A lambda is not a new scope for
this; it means the enclosing instance. This is one reason to prefer lambdas for callbacks that
mainly call back into the outer object.
Practice
- Give
Pizzaa static nestedBuilderso you can writenew Pizza.Builder().cheese().topping("olives").build().
Solution
import java.util.ArrayList;
import java.util.List;
public class PizzaDemo {
public static void main(String[] args) {
System.out.println(new Pizza.Builder().cheese().topping("olives").topping("basil").build());
System.out.println(new Pizza.Builder().build());
}
}
class Pizza {
private final boolean cheese;
private final List<String> toppings;
private Pizza(Builder b) { cheese = b.cheese; toppings = List.copyOf(b.toppings); }
public String toString() { return "Pizza{cheese=" + cheese + ", toppings=" + toppings + "}"; }
static class Builder {
private boolean cheese;
private final List<String> toppings = new ArrayList<>();
Builder cheese() { cheese = true; return this; }
Builder topping(String t) { toppings.add(t); return this; }
Pizza build() { return new Pizza(this); }
}
}
$ java PizzaDemo.java
Pizza{cheese=true, toppings=[olives, basil]}
Pizza{cheese=false, toppings=[]}
Builder is static because it builds pizzas that don’t exist yet — it needs no outer Pizza.
It can still call Pizza’s private constructor, because nested classes share access with their
enclosing class.
- Write an inner class
Playlist.Cursorwithnext()that walks the outer playlist’s songs and wraps around at the end.
Solution
import java.util.List;
public class PlaylistDemo {
public static void main(String[] args) {
Playlist p = new Playlist(List.of("Intro", "Verse", "Outro"));
Playlist.Cursor c = p.new Cursor();
for (int i = 0; i < 5; i++) System.out.print(c.next() + " ");
System.out.println();
}
}
class Playlist {
private final List<String> songs;
Playlist(List<String> songs) { this.songs = songs; }
class Cursor {
private int pos = 0;
String next() { return songs.get(pos++ % songs.size()); }
}
}
$ java PlaylistDemo.java
Intro Verse Outro Intro Verse
Cursor reads songs from its outer Playlist; two cursors on the same playlist would move
independently because each has its own pos.
- Sort
["kiwi", "Banana", "apple"]case-insensitively three ways: an anonymousComparator, a lambda, and a method reference.
Solution
import java.util.*;
public class ThreeComparators {
public static void main(String[] args) {
List<String> a = new ArrayList<>(List.of("kiwi", "Banana", "apple"));
a.sort(new Comparator<String>() {
public int compare(String x, String y) { return x.compareToIgnoreCase(y); }
});
List<String> b = new ArrayList<>(List.of("kiwi", "Banana", "apple"));
b.sort((x, y) -> x.compareToIgnoreCase(y));
List<String> c = new ArrayList<>(List.of("kiwi", "Banana", "apple"));
c.sort(String::compareToIgnoreCase);
System.out.println(a + " " + b + " " + c);
}
}
$ java ThreeComparators.java
[apple, Banana, kiwi] [apple, Banana, kiwi] [apple, Banana, kiwi]
Comparator has one abstract method, so all three forms work; the lambda and method reference
are the same behaviour with less code.
- Count anonymous button clicks without
AtomicInteger, using a field on the anonymous class itself.
Solution
public class SelfCounting {
interface Button { void click(); int count(); }
public static void main(String[] args) {
Button ok = new Button() {
private int clicks;
public void click() { clicks++; }
public int count() { return clicks; }
};
ok.click(); ok.click(); ok.click();
System.out.println("clicked " + ok.count() + " times");
}
}
$ java SelfCounting.java
clicked 3 times
A field of the anonymous object can change freely; only captured local variables must be effectively final.