Java Inner Classes β Types, Access Rules, Examples & Best Practices
Everything you need to know about Java Inner Classes β member inner class, static nested class, local inner class, anonymous inner class, enclosing instance access, variable shadowing, anonymous class vs lambda, real-world production patterns, and interview questions.
Last Updated
March 2026
Read Time
24 min
Level
Intermediate
Chapter
25 of 35
What is an Inner Class in Java?
A nested class (or inner class) in Java is a class defined inside the body of another class. The outer class is called the enclosing class. Java supports nesting classes for a fundamental reason: sometimes a class only makes sense in the context of another class, and co-locating them expresses that tight relationship clearly in the source code.
Consider a LinkedList class β it naturally has a Node concept. A Node only exists to serve the LinkedList; exposing it as a top-level public class would pollute the package's public API. Defining it as a static nested class inside LinkedList keeps it encapsulated. Similarly, event handlers, iterators, and builder objects often exist purely to serve one enclosing class β inner classes are the right tool.
Java's nested class mechanism is more powerful than simple co-location. A non-static inner class carries an implicit reference to its enclosing instance and can access all of the enclosing class's members β even private ones β without any getter methods. This is a deeper coupling than inheritance and is intentionally designed for tightly related types.
Types of Inner Classes β Overview
Before diving into each type, here is a side-by-side overview to understand the key distinctions at a glance.
Member Inner Class (Non-Static Inner Class)
A member inner class is a non-static class declared directly inside another class's body β at the same level as instance fields and methods. It has an implicit reference to an instance of its enclosing class and can access all of the enclosing instance's members β private fields, private methods, everything β directly by name. Because of this implicit reference, creating a member inner class object requires an existing outer class object.
From outside the outer class: Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); From inside the outer class: Inner inner = new Inner(); The 'outer.new Inner()' syntax explicitly associates the inner class instance with a specific outer instance.
1. Accesses all outer class members (even private) directly. 2. Holds implicit reference to enclosing instance (causes memory concern if outer is large). 3. Can be declared private, protected, public, or package-private. 4. Cannot declare static members (pre-Java 16). 5. Compiled to Outer$Inner.class with a synthetic field for the outer reference.
Use when the inner class logically belongs to a specific instance of the outer class and needs to read/modify the outer instance's private state. Classic example: Iterator implementation inside a collection class β the iterator must navigate the collection's private internal structure (array, linked list nodes).
public class BankAccount {
private String accountNumber;
private double balance;
private java.util.List<String> transactionHistory = new java.util.ArrayList<>();
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public void deposit(double amount) {
balance += amount;
transactionHistory.add("+βΉ" + amount);
}
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
transactionHistory.add("-βΉ" + amount);
}
}
// βββ Member Inner Class βββββββββββββββββββββββββββββββββββββββββββββββ
// AccountSummary needs intimate access to BankAccount's private fields
// It makes no sense to exist independently of a BankAccount instance
public class AccountSummary {
public void print() {
// β
Direct access to outer class private fields β no getters needed
System.out.println("Account : " + accountNumber);
System.out.printf ("Balance : βΉ%.2f%n", balance);
System.out.println("Transactions: " + transactionHistory.size());
transactionHistory.forEach(t -> System.out.println(" " + t));
}
public boolean isOverdrawn() {
return balance < 0; // Direct access to outer 'balance'
}
}
public static void main(String[] args) {
BankAccount acc = new BankAccount("ACC-001", 10000.0);
acc.deposit(5000);
acc.withdraw(2000);
// β
Must use outer instance to create inner class object
BankAccount.AccountSummary summary = acc.new AccountSummary();
summary.print();
// Account : ACC-001
// Balance : βΉ13000.00
// Transactions: 2
// +βΉ5000.0
// -βΉ2000.0
System.out.println("Overdrawn: " + summary.isOverdrawn()); // false
}
}Static Nested Class
A static nested class is declared with the static keyword inside another class. It does not hold any reference to an enclosing instance. Consequently, it can only directly access static members of the outer class β it has no pathway to the outer class's instance fields or methods. It is instantiated without needing an outer class object: new Outer.StaticNested().
Despite the name, a static nested class is not limited in any other way β it can have instance fields, instance methods, its own constructors, and implement interfaces. The static in its declaration means only that it has no enclosing instance dependency, making it behaviorally similar to a top-level class that happens to live inside another class for namespace/packaging reasons.
public class HttpClient {
private static final String DEFAULT_USER_AGENT = "TechSustainify/1.0";
private static int requestCount = 0;
private String baseUrl;
private int timeoutMs;
// βββ Static Nested Class β Builder for HttpClient βββββββββββββββββββββ
// Builder does NOT need access to any HttpClient instance
// It only builds configuration β static nested is the right choice
public static class Builder {
private String baseUrl = "http://localhost";
private int timeoutMs = 5000;
private String userAgent = DEFAULT_USER_AGENT; // β
outer static member OK
public Builder baseUrl(String url) {
this.baseUrl = url;
return this;
}
public Builder timeoutMs(int ms) {
this.timeoutMs = ms;
return this;
}
public Builder userAgent(String ua) {
this.userAgent = ua;
return this;
}
public HttpClient build() {
return new HttpClient(this);
}
}
// Private constructor β forces use of Builder
private HttpClient(Builder builder) {
this.baseUrl = builder.baseUrl;
this.timeoutMs = builder.timeoutMs;
}
public String get(String endpoint) {
requestCount++;
System.out.println("GET " + baseUrl + endpoint
+ " (timeout=" + timeoutMs + "ms)");
return "response-body";
}
public static int getRequestCount() { return requestCount; }
public static void main(String[] args) {
// β
No outer instance needed β new HttpClient.Builder()
HttpClient client = new HttpClient.Builder()
.baseUrl("https://api.techsustainify.com")
.timeoutMs(10000)
.build();
client.get("/users");
client.get("/products");
// GET https://api.techsustainify.com/users (timeout=10000ms)
// GET https://api.techsustainify.com/products (timeout=10000ms)
System.out.println("Total requests: " + HttpClient.getRequestCount()); // 2
}
}Local Inner Class
A local inner class is a class declared inside a method, constructor, or initializer block. Its scope is limited to the block in which it is declared β it cannot be instantiated or referenced from outside that block. Local inner classes can access: all members of the enclosing instance, and effectively final local variables of the enclosing method (variables that are assigned exactly once and never changed after that).
import java.util.*;
public class ReportGenerator {
private String companyName;
private String reportTitle;
public ReportGenerator(String companyName, String reportTitle) {
this.companyName = companyName;
this.reportTitle = reportTitle;
}
public void generateSalesReport(List<Double> monthlySales, String currency) {
// βββ Local Inner Class βββββββββββββββββββββββββββββββββββββββββββββ
// Declared inside this method β useful only within this method's logic
// Needs to format multiple things consistently throughout this method
class SalesFormatter {
// β
Access outer instance fields: companyName, reportTitle
// β
Access effectively final local var: currency
String header() {
return "=== " + companyName + " β " + reportTitle + " ===";
}
String formatAmount(double amount) {
return String.format("%s %.2f", currency, amount);
// 'currency' is effectively final β assigned once, never changed
}
String monthLabel(int month) {
String[] months = {"Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"};
return months[month - 1];
}
}
// βββ Use the local class within the same method ββββββββββββββββββββ
SalesFormatter fmt = new SalesFormatter();
System.out.println(fmt.header());
double total = 0;
double highest = Double.MIN_VALUE;
int bestMonth = 1;
for (int i = 0; i < monthlySales.size(); i++) {
double sale = monthlySales.get(i);
System.out.printf(" %-4s : %s%n",
fmt.monthLabel(i + 1), fmt.formatAmount(sale));
total += sale;
if (sale > highest) { highest = sale; bestMonth = i + 1; }
}
System.out.println("Total : " + fmt.formatAmount(total));
System.out.println("Best : " + fmt.monthLabel(bestMonth)
+ " β " + fmt.formatAmount(highest));
// 'fmt' is only visible inside generateSalesReport() β perfectly scoped
}
public static void main(String[] args) {
ReportGenerator rg = new ReportGenerator("TechSustainify", "Q1 Sales");
rg.generateSalesReport(
List.of(120000.0, 145000.0, 98000.0),
"INR"
);
// === TechSustainify β Q1 Sales ===
// Jan : INR 120000.00
// Feb : INR 145000.00
// Mar : INR 98000.00
// Total : INR 363000.00
// Best : Feb β INR 145000.00
}
}Anonymous Inner Class
An anonymous inner class is a class that is declared and instantiated in a single expression, with no class name. It must immediately extend a class or implement an interface. The class body follows the new ClassName() { ... } syntax. Anonymous classes were the primary mechanism for lightweight interface implementations in Java before lambda expressions arrived in Java 8.
import java.util.*;
public class AnonymousInnerClass {
interface Greeting {
void greet(String name);
default String prefix() { return "Hello"; }
}
abstract static class Shape {
abstract double area();
void describe() {
System.out.printf("Area = %.2f%n", area());
}
}
public static void main(String[] args) {
// βββ Anonymous class implementing an interface βββββββββββββββββββββ
Greeting formalGreeting = new Greeting() {
@Override
public void greet(String name) {
System.out.println(prefix() + ", Dr. " + name + ".");
}
// Can also override default method if needed
};
Greeting casualGreeting = new Greeting() {
@Override
public void greet(String name) {
System.out.println("Hey " + name + "!");
}
@Override
public String prefix() { return "Yo"; }
};
formalGreeting.greet("Sharma"); // Hello, Dr. Sharma.
casualGreeting.greet("Rahul"); // Hey Rahul!
// βββ Anonymous class extending an abstract class ββββββββββββββββββ
Shape circle = new Shape() {
double radius = 7.0;
@Override
double area() { return Math.PI * radius * radius; }
};
Shape rectangle = new Shape() {
double w = 5.0, h = 3.0;
@Override
double area() { return w * h; }
};
circle.describe(); // Area = 153.94
rectangle.describe(); // Area = 15.00
// βββ Anonymous class vs Lambda for single-method interface βββββββββ
List<String> names = new ArrayList<>(List.of("Zara", "Amit", "Priya"));
// Old way β anonymous class implementing Comparator
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareToIgnoreCase(b);
}
});
// Modern way β lambda (since Java 8) β same behaviour, 90% less code
names.sort((a, b) -> a.compareToIgnoreCase(b));
// Or even: names.sort(String::compareToIgnoreCase);
System.out.println(names); // [Amit, Priya, Zara]
}
}Anonymous Inner Class vs Lambda Expression
Java 8's lambda expressions replaced the most common use case of anonymous inner classes β implementing functional interfaces (interfaces with exactly one abstract method). However, anonymous classes are still necessary in scenarios where lambdas cannot be used. Understanding the distinction helps you choose the right tool.
import java.util.function.*;
public class AnonVsLambda {
// βββ Functional interface β Lambda wins βββββββββββββββββββββββββββββββ
interface Validator<T> {
boolean validate(T value);
}
// βββ Multi-method interface β Anonymous class required βββββββββββββββββ
interface DataProcessor {
void process(String data);
void onError(Exception e);
default void onComplete() { System.out.println("Done."); }
}
public static void main(String[] args) {
// β
Lambda β clean for single-method interface
Validator<String> emailValidator = value ->
value != null && value.contains("@") && value.contains(".");
Validator<Integer> ageValidator = age -> age >= 18 && age <= 120;
System.out.println(emailValidator.validate("user@example.com")); // true
System.out.println(ageValidator.validate(17)); // false
// β
Anonymous class β necessary for multi-method interface
DataProcessor processor = new DataProcessor() {
private int processedCount = 0; // β local state β lambda can't do this
@Override
public void process(String data) {
processedCount++;
System.out.println("Processing [" + processedCount + "]: " + data);
}
@Override
public void onError(Exception e) {
System.err.println("Error: " + e.getMessage());
}
@Override
public void onComplete() {
System.out.println("Processed " + processedCount + " records.");
}
};
processor.process("record-001");
processor.process("record-002");
processor.onComplete();
// Processing [1]: record-001
// Processing [2]: record-002
// Processed 2 records.
// βββ 'this' difference β critical gotcha ββββββββββββββββββββββββ
Runnable anonThis = new Runnable() {
@Override public void run() {
System.out.println(this.getClass().getSimpleName()); // AnonVsLambda$1
}
};
Runnable lambdaThis = () -> {
System.out.println(this.getClass().getSimpleName()); // AnonVsLambda
// 'this' in lambda = enclosing class (AnonVsLambda),
// NOT the lambda itself
};
}
}Access Rules β What Can See What
The access rules between inner classes and their enclosing classes are one of the most powerful (and potentially confusing) aspects of Java's nested class system. The rules differ significantly between member inner classes and static nested classes.
A member inner class can access ALL members of the enclosing class without restriction: public, protected, package-private, AND private fields and methods. It does so via its implicit reference to the enclosing instance. No getters needed. This is by design β the inner class is a trusted member of the outer class's implementation.
A static nested class can ONLY access STATIC members of the enclosing class directly. It has no enclosing instance, so instance members are inaccessible. To access instance members, it must receive an explicit outer class object reference (via constructor parameter or method parameter), just like any external class.
The outer class has FULL access to ALL members of its inner classes β including private members. This bi-directional access is a key design feature: inner classes can freely use private outer members, and the outer class can freely access private inner class members. This is the 'trusted helper' relationship.
Access from outside depends on the inner class's access modifier: public inner class is accessible from anywhere (but still requires outer instance for non-static). private inner class is accessible only from within the outer class. protected follows normal protected rules. Package-private is accessible within the package. The outer class name is used to qualify: Outer.Inner or Outer.StaticNested.
public class Outer {
private int outerPrivate = 10;
protected int outerProtected = 20;
public int outerPublic = 30;
static int outerStatic = 40;
// βββ Member Inner Class βββββββββββββββββββββββββββββββββββββββββββββββ
class Inner {
private int innerSecret = 99;
void show() {
// β
Can access ALL outer members β including private
System.out.println(outerPrivate); // 10 β direct access
System.out.println(outerProtected); // 20
System.out.println(outerPublic); // 30
System.out.println(outerStatic); // 40
}
}
// βββ Static Nested Class ββββββββββββββββββββββββββββββββββββββββββββββ
static class StaticNested {
void show() {
// β
Can access outer STATIC members
System.out.println(outerStatic); // 40
// β Cannot access outer INSTANCE members β no enclosing instance
// System.out.println(outerPrivate); // COMPILE ERROR
// System.out.println(outerPublic); // COMPILE ERROR
// β
Can access via explicit outer reference
Outer outer = new Outer();
System.out.println(outer.outerPrivate); // 10 β via explicit object
}
}
// βββ Outer accessing inner's private members βββββββββββββββββββββββββ
void accessInnerPrivate() {
Inner inner = new Inner();
System.out.println(inner.innerSecret); // β
99 β outer can see inner private
}
public static void main(String[] args) {
Outer outer = new Outer();
// Member inner β needs outer instance
Outer.Inner inner = outer.new Inner();
inner.show();
// Static nested β no outer instance needed
Outer.StaticNested nested = new Outer.StaticNested();
nested.show();
outer.accessInnerPrivate();
}
}Enclosing Instance and OuterClass.this
Every instance of a member inner class holds a hidden reference to its enclosing class instance. This reference is how the inner class accesses the outer class's fields and methods. When there is no ambiguity, unqualified names like balance or deposit() automatically resolve to the enclosing instance's members. When there is ambiguity β for example, when the inner class has a field or method with the same name as the outer β you can use OuterClassName.this to explicitly refer to the enclosing instance.
public class EventBus {
private String busName;
private java.util.List<String> eventLog = new java.util.ArrayList<>();
EventBus(String busName) { this.busName = busName; }
// βββ Member inner class with same-named field βββββββββββββββββββββββββ
class EventHandler {
private String busName = "LocalHandler"; // Shadows outer 'busName'
void handle(String eventName) {
// 'this.busName' = inner class's own field
System.out.println("Handler busName : " + this.busName);
// 'EventBus.this.busName' = outer class's field
System.out.println("Outer busName : " + EventBus.this.busName);
// β
Accessing outer method via OuterClass.this
EventBus.this.logEvent(eventName);
}
// Returns the OUTER instance β useful for chaining back
EventBus getEventBus() {
return EventBus.this; // β reference to the enclosing EventBus
}
}
private void logEvent(String event) {
eventLog.add(event);
System.out.println("[" + busName + "] Event logged: " + event);
}
public void printLog() {
System.out.println("Event log: " + eventLog);
}
public static void main(String[] args) {
EventBus bus = new EventBus("MainBus");
EventBus.EventHandler handler = bus.new EventHandler();
handler.handle("USER_LOGIN");
// Handler busName : LocalHandler
// Outer busName : MainBus
// [MainBus] Event logged: USER_LOGIN
handler.handle("PAYMENT_SUCCESS");
// Get back the outer instance from the inner class
EventBus retrievedBus = handler.getEventBus();
retrievedBus.printLog();
// Event log: [USER_LOGIN, PAYMENT_SUCCESS]
}
}Variable Shadowing in Inner Classes
Shadowing in inner classes works across three levels: the inner class's own fields, the enclosing class's fields, and local variables in the enclosing method. When a name is used inside an inner class, Java resolves it in this order: inner class's own scope β enclosing instance's scope β enclosing method's local variables. OuterClass.this.fieldName explicitly reaches the enclosing instance when shadowing occurs.
public class Shadowing {
int value = 100; // Outer instance field
class Inner {
int value = 200; // Inner field β shadows outer field
void demonstrate(int value) { // Parameter β shadows inner field
System.out.println("Parameter : " + value); // 300
System.out.println("Inner field : " + this.value); // 200
System.out.println("Outer field : " + Shadowing.this.value); // 100
}
}
void methodWithLocalVar() {
int value = 50; // local var β shadows outer field within this method
System.out.println("Local var : " + value); // 50
System.out.println("Outer field : " + this.value); // 100
// Local inner class inside this method
class LocalHelper {
void show() {
// 'value' here β is local var (50) effectively final, captured
System.out.println("Captured local : " + value); // 50
System.out.println("Outer via encl : " + Shadowing.this.value); // 100
}
}
new LocalHelper().show();
}
public static void main(String[] args) {
Shadowing outer = new Shadowing();
Shadowing.Inner inner = outer.new Inner();
inner.demonstrate(300);
// Parameter : 300
// Inner field : 200
// Outer field : 100
outer.methodWithLocalVar();
// Local var : 50
// Outer field : 100
// Captured local : 50
// Outer via encl : 100
}
}Member Inner Class vs Static Nested Class β When to Use Which
The choice between a member inner class and a static nested class is one of the most important design decisions when nesting classes. The rule is simple but often forgotten: if the nested class does not need access to any instance member of the enclosing class, make it static. This preference is backed by Effective Java (Joshua Bloch, Item 24): "If you declare a member class that does not require access to an enclosing instance, always put the static modifier in its declaration."
1. The nested class logically needs to READ or MODIFY the outer instance's private state. 2. Every instance of the nested class is fundamentally associated with one specific outer instance (e.g., an Iterator that navigates a specific collection's internal array). 3. The nested class is a helper type that is intrinsically bound to the outer class's lifecycle.
1. The nested class does NOT need access to any instance member of the outer class. 2. The nesting is for logical grouping / namespace purposes only (e.g., Node inside LinkedList, Builder inside a domain class). 3. The nested class may need to be instantiated without an existing outer instance. 4. You want to avoid the memory leak risk of holding an outer reference.
Every member inner class instance carries a synthetic private field pointing to its enclosing instance (OuterClass.this). This means: (1) Every inner instance keeps the outer instance alive β memory leak risk. (2) Serialization becomes complex β the outer instance must also be serializable. (3) Slightly more memory per inner instance. (4) Cannot be used independently β always tied to an outer object. None of these apply to static nested classes.
Common Mistakes & Pitfalls
These mistakes with inner classes are consistently found in Java code and often result in compile errors, memory leaks, or subtle runtime bugs.
public class InnerClassMistakes {
String data = "outer data";
// β MISTAKE 1: Trying to instantiate member inner class without outer instance
class MemberInner { void show() { System.out.println(data); } }
public static void main(String[] args) {
// β COMPILE ERROR: no enclosing instance of type InnerClassMistakes
// MemberInner m = new MemberInner();
// β
Fix: create outer instance first
InnerClassMistakes outer = new InnerClassMistakes();
MemberInner m = outer.new MemberInner(); // β correct syntax
m.show();
}
// β MISTAKE 2: Using non-effectively-final local var in inner class
void brokenMethod() {
int count = 0;
Runnable r = new Runnable() {
@Override public void run() {
// count++; // β COMPILE ERROR: local variable count is accessed
// // from an inner class; needs to be final or effectively final
System.out.println(count); // β
reading is OK if count is never changed
}
};
// count = 1; // This would break effective finality β compile error in run()
r.run();
}
// β MISTAKE 3: Confusing 'this' in anonymous class vs lambda
void thisConfusion() {
Runnable anonClass = new Runnable() {
@Override public void run() {
// 'this' here = the anonymous Runnable instance
System.out.println(this.getClass().getName()); // ...InnerClassMistakes$2
}
};
Runnable lambda = () -> {
// 'this' here = the ENCLOSING InnerClassMistakes instance
System.out.println(this.getClass().getName()); // InnerClassMistakes
};
}
// β MISTAKE 4: Declaring static member inside non-static inner class (pre Java 16)
class WrongInner {
// static int counter = 0; // β COMPILE ERROR before Java 16
static final int MAX = 100; // β
static final constant β always allowed
}
// β MISTAKE 5: Forgetting static on nested class that doesn't need outer access
// BAD β holds unnecessary reference to InnerClassMistakes instance
class UnnecessaryMemberInner {
int compute(int x) { return x * 2; } // Never touches outer fields!
}
// β
Fix: make it static β no outer reference needed
static class BetterStaticNested {
int compute(int x) { return x * 2; }
}
}Bad Practices & Anti-Patterns
These inner class anti-patterns frequently surface in production code reviews and lead to memory issues, maintainability problems, or design violations.
The single most common inner class anti-pattern β using a member inner class when the class never accesses any outer instance member. Every instance silently holds an outer reference, preventing GC. Effective Java (Item 24) is explicit: 'if a member class does not require access to an enclosing instance, always put static on it.' Treat static as the default and add non-static only when outer instance access is genuinely required.
Passing 'this' (a member inner class instance) to an event bus, listener registry, or observable β and never removing it β is one of the most common Java memory leaks. The registry holds a reference to the inner instance, which holds a reference to the outer, preventing GC of both. Always pair register() with unregister(), and use weak references (WeakReference<Listener>) in listener lists when object lifecycle is uncertain.
Using an anonymous inner class with 20+ lines of logic makes the enclosing method illegible. If the anonymous class body is large, it deserves to be a named class β either a private static nested class, a separate package-private class, or a private method that returns the interface via a lambda. The rule: anonymous classes should be short and self-evident. If you need to write a comment inside one explaining what it does, extract it to a named class.
Nesting inner classes more than two levels deep (class A { class B { class C {} } }) creates code that is almost impossible to read and reason about. Qualified names like A.B.C and reference patterns like A.B.this become incomprehensible. If you find yourself nesting three or more levels, redesign β flatten the hierarchy, use composition, or extract some classes to top-level or package-private classes.
In Java 8+ code, writing 'new Comparator<String>() { @Override public int compare(String a, String b) { return a.compareTo(b); } }' instead of '(a, b) -> a.compareTo(b)' is verbose and dated. Lambdas are the modern replacement for anonymous classes implementing single-method interfaces. Code reviewers in 2026 will flag anonymous class usage for functional interfaces as a style violation. Always prefer lambdas (or method references) for functional interface implementations.
Exposing a public member inner class in a library's public API forces API consumers to handle the 'outer.new Inner()' instantiation syntax, which is unfamiliar and error-prone. Users also inherit the outer instance dependency implicitly. Library APIs should expose public static nested classes (or top-level classes) β never public non-static inner classes. If the inner class must be exposed publicly, a factory method on the outer class is a cleaner API: 'outer.createHelper()' returns the inner without exposing the syntax.
Real-World Production Code Examples
The following examples show idiomatic inner class usage found in real enterprise Java codebases β each demonstrating the appropriate type for its design scenario.
package com.techsustainify.collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class SimpleLinkedList<T> implements Iterable<T> {
// βββ Static Nested Class: Node βββββββββββββββββββββββββββββββββββββββββ
// Node doesn't need to access the LinkedList instance β pure data holder
// static is the correct choice
private static class Node<T> {
T data;
Node<T> next;
Node(T data) {
this.data = data;
this.next = null;
}
}
private Node<T> head;
private int size;
public void add(T item) {
Node<T> newNode = new Node<>(item); // β
No outer instance needed for Node
if (head == null) {
head = newNode;
} else {
Node<T> current = head;
while (current.next != null) current = current.next;
current.next = newNode;
}
size++;
}
public int size() { return size; }
// βββ Member Inner Class: ListIterator βββββββββββββββββββββββββββββββββ
// Iterator MUST access 'head' β the LinkedList instance's private field
// Non-static inner class is the correct choice here
private class ListIterator implements Iterator<T> {
private Node<T> current = head; // β
Accesses outer 'head' directly
@Override
public boolean hasNext() {
return current != null;
}
@Override
public T next() {
if (!hasNext()) throw new NoSuchElementException();
T data = current.data;
current = current.next;
return data;
}
}
@Override
public Iterator<T> iterator() {
return new ListIterator(); // Inner class β can only create from inside outer
}
public static void main(String[] args) {
SimpleLinkedList<String> list = new SimpleLinkedList<>();
list.add("Delhi");
list.add("Mumbai");
list.add("Bangalore");
for (String city : list) {
System.out.println(city);
}
// Delhi
// Mumbai
// Bangalore
}
}package com.techsustainify.notification;
import java.util.*;
import java.util.function.Consumer;
public class NotificationManager {
// βββ Static Nested Class: Config ββββββββββββββββββββββββββββββββββββββ
// Config has no dependency on any NotificationManager instance
public static class Config {
private boolean emailEnabled = true;
private boolean smsEnabled = false;
private int retryAttempts = 3;
private long retryDelayMs = 1000L;
public Config emailEnabled(boolean v) { this.emailEnabled = v; return this; }
public Config smsEnabled(boolean v) { this.smsEnabled = v; return this; }
public Config retryAttempts(int n) { this.retryAttempts = n; return this; }
public Config retryDelayMs(long ms) { this.retryDelayMs = ms; return this; }
public boolean isEmailEnabled() { return emailEnabled; }
public boolean isSmsEnabled() { return smsEnabled; }
public int getRetryAttempts(){ return retryAttempts; }
}
// βββ Functional interface for notification handler βββββββββββββββββββββ
public interface NotificationHandler {
boolean deliver(String recipient, String message);
default String channelName() { return "unknown"; }
}
private Config config;
private List<NotificationHandler> handlers = new ArrayList<>();
public NotificationManager(Config config) {
this.config = config;
setupHandlers(); // registers anonymous class handlers
}
private void setupHandlers() {
if (config.isEmailEnabled()) {
// βββ Anonymous class β implements 2 methods (can't use lambda) β
handlers.add(new NotificationHandler() {
@Override
public boolean deliver(String recipient, String message) {
System.out.printf("[EMAIL] To: %s | Msg: %s%n",
recipient, message);
return true;
}
@Override
public String channelName() { return "EMAIL"; }
});
}
if (config.isSmsEnabled()) {
handlers.add(new NotificationHandler() {
@Override
public boolean deliver(String recipient, String message) {
System.out.printf("[SMS] To: %s | Msg: %s%n",
recipient, message);
return true;
}
@Override
public String channelName() { return "SMS"; }
});
}
}
public void notify(String recipient, String message) {
for (NotificationHandler handler : handlers) {
boolean sent = false;
for (int attempt = 1; attempt <= config.getRetryAttempts(); attempt++) {
sent = handler.deliver(recipient, message);
if (sent) break;
}
System.out.println(handler.channelName()
+ (sent ? " delivered" : " failed"));
}
}
public static void main(String[] args) {
// β
Static nested Config β no outer instance needed
NotificationManager.Config cfg = new NotificationManager.Config()
.emailEnabled(true)
.smsEnabled(true)
.retryAttempts(2);
NotificationManager manager = new NotificationManager(cfg);
manager.notify("user@example.com", "Your order has shipped!");
// [EMAIL] To: user@example.com | Msg: Your order has shipped!
// EMAIL delivered
// [SMS] To: user@example.com | Msg: Your order has shipped!
// SMS delivered
}
}Inner Class Type Decision Flowchart
Use this decision flowchart when you need to define a class inside another class and are choosing which type to use.
Code Execution Flow β from source to output
Java Inner Classes Interview Questions β Beginner to Advanced
These questions are consistently asked in Java mid-level and senior interviews, OCPJP certification exams, and design-focused technical rounds.
Practice Questions β Test Your Inner Class Knowledge
Attempt each question independently before reading the answer β active recall strengthens understanding far more effectively than passive reading.
1. What is the output? class Outer { int x = 10; class Inner { int x = 20; void show(int x) { System.out.println(x); System.out.println(this.x); System.out.println(Outer.this.x); } } } new Outer().new Inner().show(30);
Easy2. Will this compile? Explain. class Outer { static class Nested { int outerVal = 5; void show() { System.out.println(outerVal); } } public static void main(String[] args) { new Outer.Nested().show(); } }
Easy3. Fix the compile error: public class Container { private int value = 42; class Helper { void printValue() { System.out.println(value); } } public static void main(String[] args) { Helper h = new Helper(); h.printValue(); } }
Easy4. What is the output and why? class Outer { static int count = 0; static class Counter { void increment() { count++; } int get() { return count; } } } public class Test { public static void main(String[] args) { Outer.Counter c1 = new Outer.Counter(); Outer.Counter c2 = new Outer.Counter(); c1.increment(); c1.increment(); c2.increment(); System.out.println(c1.get() + " " + c2.get()); } }
Medium5. Will this code compile? What is the error if any? void method() { int total = 0; Runnable r = new Runnable() { @Override public void run() { total += 10; // modifying local variable System.out.println(total); } }; r.run(); }
Medium6. What does this print and what inner class concept does it demonstrate? interface Printer { void print(); } class Demo { String msg = "outer"; Printer getPrinter() { String msg = "local"; // effectively final return new Printer() { String msg = "inner"; @Override public void print() { System.out.println(msg); // Line 1 System.out.println(this.msg); // Line 2 System.out.println(Demo.this.msg); // Line 3 } }; } public static void main(String[] args) { new Demo().getPrinter().print(); } }
Hard7. Design a Cache class with a static nested Entry class. Entry should hold a key (String), value (Object), and expiry timestamp (long). Cache should have a method get(String key) that returns the value if not expired, null otherwise.
Hard8. What is wrong with this Android-style code? How would you fix it? class MainActivity { private View rootView; void loadData() { new Thread(new Runnable() { @Override public void run() { // fetch data from network... rootView.setVisibility(View.VISIBLE); // access outer field } }).start(); } }
HardConclusion β Inner Classes: Precision Tools for Encapsulation and Cohesion
Java's four inner class types give you precision tools for expressing the exact degree of coupling between related types. Static nested classes are the workhorses of Java's standard library β Map.Entry, LinkedList.Node, and countless Builders are static nested classes. They provide logical grouping with zero coupling overhead. Member inner classes power Java's iterator pattern, giving iterators intimate, efficient access to collection internals without exposing that internals to the world. Anonymous classes remain the solution for multi-method one-off implementations even in the lambda era. Local inner classes are the rarest but occasionally perfect for method-scoped multi-method logic.
The single most impactful habit to develop: always default to static nested classes and only relax to non-static when you genuinely need outer instance access. This prevents memory leaks, simplifies serialization, and keeps instantiation straightforward. As Effective Java puts it β the static modifier is a signal of independence, and independence is almost always a virtue in software design.
Your next step: Java Abstract Classes β where you will see how abstract classes serve as the perfect base for member inner class implementations, and how the template method pattern leverages the combination of inheritance and inner class access patterns you've just mastered. β