Java Inner Classes - Complete Tutorial
Master Java inner classes: member, static nested, local, and anonymous classes—with comparison tables, real-world examples, access rules, quick reference, and best practices.
What are Inner Classes?
Inner classes (also called nested classes) are classes defined inside another class. They let you logically group types that are only used in one place, which improves encapsulation and keeps related code together.
Why Use Inner Classes?
When a helper class belongs to only one outer type, nesting it avoids polluting the package and makes the relationship obvious.
class LinkedList {
Node head;
}
class Node { // Only used by LinkedList, but declared at top level
int data;
Node next;
}
class LinkedList {
class Node { // Node lives inside LinkedList where it belongs
int data;
Node next;
}
Node head;
}
Key idea
Inner classes express has-a / part-of relationships: a Node is part of a LinkedList, not a standalone public type in the package.
Types of Inner Classes
Java supports four kinds of nested classes. Choose based on whether you need an outer instance, static context, method scope, or a one-off implementation.
| Type | Static? | Can access outer instance? | When to use |
|---|---|---|---|
| Member Inner Class | No | Yes | When the nested class needs access to outer instance fields and methods |
| Static Nested Class | Yes | No (only static outer members) | When the nested class does not need an outer instance |
| Local Inner Class | No | Yes | Inside a method or block, for limited scope |
| Anonymous Inner Class | No | Yes | One-time implementation of an interface or extension of a class |
Member Inner Class
A non-static class defined at the member level inside another class. Each inner object is tied to an outer object and can access all outer members, including private ones.
Basic Example
public class OuterClass {
private String outerField = "Outer Field";
private static String staticField = "Static Field";
// Member Inner Class
class InnerClass {
private String innerField = "Inner Field";
public void display() {
// Can access all members of outer class (including private)
System.out.println("Accessing: " + outerField);
System.out.println("Accessing: " + staticField);
System.out.println("Accessing: " + innerField);
}
}
public void createInner() {
InnerClass inner = new InnerClass();
inner.display();
}
public static void main(String[] args) {
// Creating member inner class requires outer instance
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.display();
}
}
outer.new InnerClass() — not new OuterClass.InnerClass() alone for member inner classes.Complete Example: Employee Management System
import java.util.*;
public class Company {
private String companyName;
private List<Employee> employees = new ArrayList<>();
public Company(String companyName) {
this.companyName = companyName;
}
// Member Inner Class
class Employee {
private int id;
private String name;
private double salary;
public Employee(int id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
public void displayInfo() {
System.out.println("Company: " + companyName);
System.out.println("Employee ID: " + id);
System.out.println("Employee Name: " + name);
System.out.println("Salary: $" + salary);
}
public void giveRaise(double percentage) {
salary += salary * (percentage / 100);
System.out.println(name + " received " + percentage + "% raise");
}
public void addToCompany() {
employees.add(this);
}
}
public void listAllEmployees() {
System.out.println("\n--- Employees of " + companyName + " ---");
for (Employee emp : employees) {
emp.displayInfo();
System.out.println("---");
}
}
public static void main(String[] args) {
Company techCorp = new Company("TechCorp");
Company.Employee emp1 = techCorp.new Employee(101, "Alice", 75000);
Company.Employee emp2 = techCorp.new Employee(102, "Bob", 68000);
emp1.addToCompany();
emp2.addToCompany();
emp1.giveRaise(10);
techCorp.listAllEmployees();
}
}
Nested Inner Classes
Inner classes can be nested multiple levels deep. Each level can access members of its enclosing classes.
public class University {
private String name = "State University";
class Department {
private String deptName;
Department(String deptName) {
this.deptName = deptName;
}
class Student {
private String studentName;
Student(String studentName) {
this.studentName = studentName;
}
void display() {
System.out.println("University: " + name);
System.out.println("Department: " + deptName);
System.out.println("Student: " + studentName);
}
}
Student createStudent(String name) {
return new Student(name);
}
}
public static void main(String[] args) {
University uni = new University();
University.Department csDept = uni.new Department("Computer Science");
University.Department.Student alice = csDept.new Student("Alice");
alice.display();
}
}
Static Nested Class
A static class nested inside another class. It behaves like a top-level class but is packaged inside the outer type for organization. It cannot access non-static outer members directly.
Basic Example
public class OuterClass {
private String instanceField = "Instance";
private static String staticField = "Static";
static class StaticNestedClass {
private String nestedField = "Nested Field";
public void display() {
// Cannot access instance members of outer class
// System.out.println(instanceField); // ERROR!
System.out.println("Accessing: " + staticField);
System.out.println("Accessing: " + nestedField);
}
public static void staticMethod() {
System.out.println("Static method in nested class");
}
}
public static void main(String[] args) {
OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass();
nested.display();
OuterClass.StaticNestedClass.staticMethod();
}
}
Practical Example: Calculator with Operation Constants
public class Calculator {
private double result = 0;
public static class MathConstants {
public static final double PI = 3.14159;
public static final double E = 2.71828;
public static final double GOLDEN_RATIO = 1.61803;
public static void displayConstants() {
System.out.println("PI: " + PI);
System.out.println("E: " + E);
System.out.println("Golden Ratio: " + GOLDEN_RATIO);
}
}
public static class UnitConverter {
public static double kgToLbs(double kg) {
return kg * 2.20462;
}
public static double lbsToKg(double lbs) {
return lbs / 2.20462;
}
public static double celsiusToFahrenheit(double celsius) {
return (celsius * 9 / 5) + 32;
}
public static double fahrenheitToCelsius(double fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
}
public static class Geometry {
public static double circleArea(double radius) {
return MathConstants.PI * radius * radius;
}
public static double rectangleArea(double length, double width) {
return length * width;
}
public static double triangleArea(double base, double height) {
return 0.5 * base * height;
}
}
public static void main(String[] args) {
System.out.println("Math Constants:");
Calculator.MathConstants.displayConstants();
System.out.println("\nUnit Conversions:");
System.out.println("70 kg = " + Calculator.UnitConverter.kgToLbs(70) + " lbs");
System.out.println("25°C = " + Calculator.UnitConverter.celsiusToFahrenheit(25) + "°F");
System.out.println("\nGeometry:");
System.out.println("Circle area (r=5): " + Calculator.Geometry.circleArea(5));
System.out.println("Rectangle area (4x6): " + Calculator.Geometry.rectangleArea(4, 6));
}
}
Builder Pattern with Static Nested Class
public class User {
private final String username;
private final String email;
private final int age;
private final String phone;
private final String address;
private User(Builder builder) {
this.username = builder.username;
this.email = builder.email;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public static class Builder {
private final String username;
private final String email;
private int age = 0;
private String phone = "";
private String address = "";
public Builder(String username, String email) {
this.username = username;
this.email = email;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
@Override
public String toString() {
return String.format(
"User{username='%s', email='%s', age=%d, phone='%s', address='%s'}",
username, email, age, phone, address);
}
public static void main(String[] args) {
User user1 = new User.Builder("john_doe", "john@example.com")
.age(25)
.phone("555-1234")
.address("123 Main St")
.build();
User user2 = new User.Builder("jane_smith", "jane@example.com")
.age(30)
.build();
System.out.println(user1);
System.out.println(user2);
}
}
Local Inner Class
A class defined inside a method, constructor, or block. It is visible only within that block and is often used when the helper type is needed in one place only.
public class OuterClass {
private String outerField = "Outer";
public void someMethod() {
final String localVar = "Local Variable";
String effectivelyFinal = "Effectively Final";
class LocalClass {
private String localInnerField = "Local Inner";
public void display() {
System.out.println("Outer field: " + outerField);
System.out.println("Local variable: " + localVar);
System.out.println("Effectively final: " + effectivelyFinal);
System.out.println("Local inner field: " + localInnerField);
}
public void modifyOuter() {
outerField = "Modified by local class";
}
}
LocalClass local = new LocalClass();
local.display();
local.modifyOuter();
System.out.println("After modification: " + outerField);
}
public static void main(String[] args) {
new OuterClass().someMethod();
}
}
Effectively final
Local and anonymous classes can read local variables only if they are final or effectively final (never reassigned after initialization).
Anonymous Inner Class
An anonymous inner class has no name. You define and instantiate it in one expression — ideal for one-time implementations of an interface or extension of a class.
public class AnonymousDemo {
interface Greeting {
void greet();
}
abstract class Animal {
abstract void makeSound();
}
public static void main(String[] args) {
// Anonymous class implementing an interface
Greeting greeting = new Greeting() {
@Override
public void greet() {
System.out.println("Hello from anonymous class!");
}
};
greeting.greet();
// Anonymous class extending an abstract class
Animal dog = new Animal() {
@Override
void makeSound() {
System.out.println("Woof! Woof!");
}
};
dog.makeSound();
// Anonymous class extending Thread
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Running in anonymous thread");
}
};
thread.start();
// Anonymous class implementing Runnable
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Runnable in anonymous class");
}
};
new Thread(task).start();
}
}
() -> { } lambdas instead of anonymous classes when the logic is short.Accessing Outer Class Members
From inside a member, local, or anonymous inner class, you can access outer instance members directly. When names clash between inner and outer, use OuterClass.this to refer to the enclosing instance.
public class Outer {
private int value = 10;
private String name = "Outer";
class Inner {
private int value = 20;
void show() {
System.out.println("Inner value: " + value); // 20
System.out.println("Outer value: " + Outer.this.value); // 10
System.out.println("Outer name: " + Outer.this.name);
}
Outer getOuter() {
return Outer.this;
}
}
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.show();
System.out.println("Same outer? " + (inner.getOuter() == outer));
}
}
Static nested classes cannot use Outer.this because they have no enclosing instance. They may only access static members of the outer class.
Practical Examples
The examples above cover real-world patterns you will see in production code:
- Member inner class —
Company.Employeeties employees to a company instance; nestedUniversity.Department.Studentmodels multi-level containment. - Static nested class —
Calculatorgroups constants and utilities;User.Builderimplements the builder pattern without exposing a public constructor. - Local inner class — helper logic scoped to a single method in
OuterClass.someMethod(). - Anonymous inner class — quick callbacks for interfaces, abstract classes, and
Thread/Runnable.
Together, these show when to nest for encapsulation, when to use static for independence, and when anonymous or lambda syntax is enough for a one-off behavior.
Common Use Cases
- Event listeners — button clicks, mouse events (
ActionListener,MouseListener) - Adapters — provide default empty implementations; override only what you need
- Builders — fluent object construction via static nested
Builderclasses - Data structures —
NodeinsideLinkedList, iterators inside collections - Callbacks — pass behavior into APIs (
Runnable, comparators, custom handlers)
Best Practices and Pitfalls
DO
- Use static nested classes when the nested type does not need outer instance state
- Keep inner classes small and focused on one responsibility
- Use inner classes to hide implementation details from the rest of the package
- Prefer lambda expressions (Java 8+) for simple functional-interface callbacks
- Use
Outer.thiswhen inner and outer fields share the same name
DON'T
- Don't use non-static inner classes in long-lived objects if they capture the outer instance unnecessarily (memory leak risk)
- Don't nest too deeply — more than two levels hurts readability
- Don't use anonymous classes when a named class or lambda is clearer
- Don't try to modify local variables from inner classes — they must be effectively final
- Don't confuse
Outer.Inner(static nested) withouter.new Inner()(member inner)
Runnable, Comparator<T>, or ActionListener, use () -> { } when you only need a short implementation. Keep anonymous classes when you override multiple methods or extend a class with state.// Anonymous inner class (older style)
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// Lambda (Java 8+) — preferred when short
Runnable r2 = () -> System.out.println("Hello");
Quick Reference Card
// 1. MEMBER INNER CLASS
class Outer {
class Inner {
void method() {
Outer.this.outerMethod();
}
}
}
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
// 2. STATIC NESTED CLASS
class Outer {
static class Nested { }
}
Outer.Nested nested = new Outer.Nested();
// 3. LOCAL INNER CLASS
class Outer {
void method() {
class Local { }
Local local = new Local();
}
}
// 4. ANONYMOUS INNER CLASS
Runnable r = new Runnable() {
@Override
public void run() { }
};
// 5. ACCESSING OUTER THIS
class Outer {
class Inner {
Outer getOuter() {
return Outer.this;
}
}
}
// 6. MODIFYING LOCAL VARIABLES (workaround)
class Outer {
void method() {
int[] counter = {0};
Runnable r = new Runnable() {
public void run() {
counter[0]++;
}
};
}
}
Key Takeaways
- Four types of inner classes: Member, Static Nested, Local, Anonymous
- Member inner classes require an outer instance and can access all outer members
- Static nested classes do not need an outer instance and can only access static outer members
- Local inner classes are defined inside methods and have limited scope
- Anonymous inner classes provide quick, one-time implementations
- Use static nested classes to avoid memory leaks and for better performance when no outer state is needed
- Inner classes increase encapsulation by hiding implementation details
- Accessing outer members from inner classes uses
OuterClass.thissyntax - Local and anonymous classes can only access effectively final local variables
- Consider lambda expressions instead of anonymous classes in Java 8+
Summary
Choose the nested type by whether you need outer instance access: member/local/anonymous when yes, static nested when no. Use builders and data-structure nodes as static or member nested classes, and prefer lambdas for simple one-method callbacks.