Java Tutorial
🔍

Java Inner Classes

Java Inner Classes

An inner class is a class defined inside another class. That single sentence covers the syntax, but not the reason it exists - and the reason matters more than the syntax. Java lets you nest a class inside another when that class only makes sense in the context of its enclosing class: an iterator that only makes sense for the collection it iterates, a builder that only makes sense for the object it builds, a comparator you need exactly once at the point you sort something. Inner classes are how Java keeps tightly related code physically close, while still giving you the full power of a class - fields, methods, constructors, even its own inheritance.

What Are Inner Classes?

A class declared inside the body of another class is, in Java's specification, a nested class. Nested classes split into two families depending on one keyword: whether static is present.

class Outer {

    class MemberInner { }          <- non-static - this is an "inner class"

    static class StaticNested { }  <- static - this is a "static nested class",
                                       NOT technically an "inner class"

    void someMethod() {
        class LocalInner { }       <- local class - also an "inner class"
    }

    void anotherMethod() {
        Runnable r = new Runnable() {  <- anonymous class - also an "inner class"
            public void run() { }
        };
    }
}

The JLS draws a precise line here: inner class specifically means a non-static nested class - member inner classes, local classes, and anonymous classes. A static nested class is a nested class but not, strictly speaking, an inner class. In everyday conversation and in most interviews, people use "inner class" loosely to mean any nested class - but knowing the precise distinction is worth having, because it is exactly the static keyword that determines the single most important behavioral difference between the two families: whether the nested class carries a hidden reference to an instance of its enclosing class.

Basic Overview - The Four Forms at a Glance

FORM 1 - MEMBER (NON-STATIC) INNER CLASS
  Declared      : inside the class body, without the static keyword
  Instantiated  : outerInstance.new InnerClass()
  Fresher view  : a helper class that belongs to one specific object
  Deeper view   : carries a hidden "this dollar zero" reference to its
                  outer instance - can read and write the outer
                  instance's fields directly, including private ones

FORM 2 - STATIC NESTED CLASS
  Declared      : inside the class body, with the static keyword
  Instantiated  : new OuterClass.NestedClass()
  Fresher view  : a regular class that just lives inside another
                  class's namespace, for organization
  Deeper view   : behaves exactly like a top-level class - no outer
                  instance required, no hidden reference, can exist
                  even if zero Outer instances exist anywhere

FORM 3 - LOCAL INNER CLASS
  Declared      : inside a method body, between statements
  Instantiated  : new LocalClassName() - only reachable within that method
  Fresher view  : a class you only need for one method, so you write
                  it right there instead of as a separate file
  Deeper view   : captures effectively-final local variables BY VALUE
                  at construction time - its scope ends with the
                  enclosing block, but instances can outlive the method
                  if returned or stored

FORM 4 - ANONYMOUS INNER CLASS
  Declared      : inline, as part of a "new" expression - has no name
  Instantiated  : at the exact point it is declared - declaration and
                  instantiation are the same statement
  Fresher view  : a one-time, throwaway implementation of an interface
                  or abstract class, written exactly where it is used
  Deeper view   : compiles to OuterClass dollar N dot class (N = 1, 2,
                  3...) - since Java 8, single-method cases are often
                  replaced by lambda expressions

A fresher reading this for the first time mainly needs forms 1 and 4 - they show up constantly in iterator implementations and inline comparator or listener code. Someone with a few years of Java behind them will recognize form 2 as the workhorse for builders and data-holder types, and form 3 as the rarest of the four - useful, but something most developers write a handful of times across an entire career.

Why Inner Classes Matter

The honest answer is encapsulation taken one level further than most beginners realize. A private field hides data from other classes. A private (or even package-private) inner class hides an entire class from other classes - the helper type exists, does real work, but nothing outside the enclosing class even knows it is there.

Consider how Iterator implementations are written across the JDK and in well-designed application code. An ArrayList needs an iterator; that iterator needs access to ArrayList's internal array and a cursor position. The iterator is conceptually part of ArrayList - no other class has any business creating one independently, and no other class needs to know its internal fields. A non-static inner class is the natural fit: it gets direct access to ArrayList's private state without a single getter, and it cannot be instantiated by anyone who does not already have an ArrayList to iterate.

The same reasoning explains why Map.Entry is a nested interface inside Map, why builder classes are almost always static nested classes inside the type they build, and why a one-off Comparator written for a single sort call does not deserve its own file. During code reviews, seniors commonly flag a small, single-purpose top-level class that exists only to support one other class - "does this need to be its own file, or does it belong inside the class that actually uses it" is a question worth asking before creating a new top-level type.

There is a second, less obvious benefit: inner classes let you express a relationship in the type system itself. SeatLayout.Builder and SeatLayout.SeatIterator are visibly, structurally part of SeatLayout - the nesting communicates ownership and scope before a reader even looks at the implementation.

How Inner Classes Work

Member (Non-Static) Inner Class

A member inner class is declared like any other field or method of the enclosing class, just without static. Every instance of it is tied to exactly one instance of the enclosing class - the one that created it.

SYNTAX:
  class Outer {
      class Inner {
          // can access Outer's instance fields and methods directly
      }
  }

  Outer outer = new Outer();
  Outer.Inner inner = outer.new Inner();   <- the "outer.new" form

The reason this needs an outer instance is structural, not arbitrary: the compiler generates a hidden field inside every member inner class - referred to in bytecode as this$0 - that points back to the enclosing instance. That field has to be set to something at construction time, and outer.new Inner() is how you supply it. The library example below shows both the access to outer fields and the Outer.this syntax for resolving naming conflicts.

1// File: MemberInnerClassDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class MemberInnerClassDemo { 7 8 static class Library { 9 private final String libraryName; 10 private final List<String> books = new ArrayList<>(); 11 12 Library(String libraryName) { 13 this.libraryName = libraryName; 14 } 15 16 void addBook(String title) { 17 books.add(title); 18 } 19 20 // Non-static inner class - tied to one specific Library instance. 21 // It reads 'books' and 'libraryName' directly because every 22 // CardCatalog instance carries a hidden reference to its Library. 23 class CardCatalog { 24 void printCatalog() { 25 System.out.println("Catalog for: " + libraryName); 26 for (String title : books) { 27 System.out.println(" - " + title); 28 } 29 } 30 31 // Outer.this is needed when a local name shadows the outer field 32 void printLibraryNameExplicit(String libraryName) { 33 // 'libraryName' here refers to the PARAMETER, not the outer field 34 System.out.println("Parameter value : " + libraryName); 35 System.out.println("Outer field value: " + Library.this.libraryName); 36 } 37 } 38 } 39 40 public static void main(String[] args) { 41 Library library = new Library("City Central Library"); 42 library.addBook("Effective Java"); 43 library.addBook("Clean Code"); 44 45 // Creating a non-static inner class instance requires an outer instance 46 Library.CardCatalog catalog = library.new CardCatalog(); 47 catalog.printCatalog(); 48 49 System.out.println(); 50 catalog.printLibraryNameExplicit("Branch Library"); 51 } 52}
Output:
Catalog for: City Central Library
  - Effective Java
  - Clean Code

Parameter value  : Branch Library
Outer field value: City Central Library

Static Nested Class

Add static to a nested class and it stops being an "inner" class in the strict sense - it no longer carries any reference to an enclosing instance, and behaves like a regular top-level class that simply lives inside another class's namespace.

SYNTAX:
  class Outer {
      static class Nested {
          // NO implicit access to Outer's instance fields/methods
          // can access Outer's STATIC members directly
      }
  }

  Outer.Nested nested = new Outer.Nested();   <- "Outer.Nested" form - NO
                                                  Outer instance required
1// File: StaticNestedClassDemo.java 2 3public class StaticNestedClassDemo { 4 5 static class Employee { 6 private final String name; 7 private final Address address; 8 9 Employee(String name, Address address) { 10 this.name = name; 11 this.address = address; 12 } 13 14 void printDetails() { 15 System.out.println(name + " works from " + address.city() + ", " + address.pincode()); 16 } 17 18 // Static nested class - does not need an Employee instance to exist. 19 // It groups Address logically under Employee without being tied 20 // to any specific employee's state. 21 static class Address { 22 private final String city; 23 private final String pincode; 24 25 Address(String city, String pincode) { 26 this.city = city; 27 this.pincode = pincode; 28 } 29 30 String city() { return city; } 31 String pincode() { return pincode; } 32 33 // The following method would NOT COMPILE if uncommented: 34 // String describeOwner() { 35 // return "Employee: " + name; 36 // // 'name' is an instance field of Employee - Address is 37 // // static, so it has no implicit Employee instance to 38 // // read 'name' from 39 // } 40 } 41 } 42 43 public static void main(String[] args) { 44 // Created with Outer.Nested syntax - no Employee instance involved 45 Employee.Address address = new Employee.Address("Bengaluru", "560034"); 46 Employee employee = new Employee("Ananya", address); 47 employee.printDetails(); 48 } 49}
Output:
Ananya works from Bengaluru, 560034

Local Inner Class

A local class is declared inside a method body, between statements - its name is only visible within that block. What makes local classes interesting is what they can capture from the surrounding method.

SYNTAX:
  void someMethod(int parameter) {
      int localVariable = parameter * 2;

      class LocalHelper {
          int compute() {
              return localVariable + 1;   <- captures 'localVariable'
          }
      }

      LocalHelper helper = new LocalHelper();
      // LocalHelper is NOT visible outside this method
  }

A local class can read any local variable or parameter of the enclosing method - but only if that variable is effectively final, meaning it is assigned once and never reassigned afterward. The compiler copies the variable's current value into the local class instance at construction time; if the original variable could change later, that copy would silently go stale, so Java disallows the situation entirely rather than risk it.

1// File: LocalInnerClassDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class LocalInnerClassDemo { 7 8 // Returns product names whose price falls within 'tolerance' of 'targetPrice' 9 static List<String> findSimilarlyPriced( 10 List<Double> prices, List<String> names, double targetPrice, double tolerance) { 11 12 // Local inner class - exists ONLY inside this method. 13 // It captures 'targetPrice' and 'tolerance' from the enclosing 14 // method - both are effectively final, so the capture is valid. 15 class PriceMatcher { 16 boolean matches(double price) { 17 return Math.abs(price - targetPrice) <= tolerance; 18 } 19 } 20 21 PriceMatcher matcher = new PriceMatcher(); 22 List<String> matched = new ArrayList<>(); 23 for (int i = 0; i < prices.size(); i++) { 24 if (matcher.matches(prices.get(i))) { 25 matched.add(names.get(i)); 26 } 27 } 28 return matched; 29 } 30 31 public static void main(String[] args) { 32 List<String> names = List.of("Wireless Mouse", "Keyboard", "Monitor Stand", "USB Cable"); 33 List<Double> prices = List.of(799.0, 1499.0, 850.0, 199.0); 34 35 List<String> similar = findSimilarlyPriced(prices, names, 800.0, 100.0); 36 System.out.println("Products near Rs.800 (+/- Rs.100):"); 37 similar.forEach(name -> System.out.println(" " + name)); 38 } 39}
Output:
Products near Rs.800 (+/- Rs.100):
  Wireless Mouse
  Monitor Stand

Anonymous Inner Class

An anonymous class has no name - its declaration and its single instantiation happen in the same expression, immediately after new. It is the most situational of the four forms: useful exactly when you need a one-off implementation of an interface or abstract class and do not want a separate named type cluttering the codebase for something used once.

SYNTAX:
  SomeInterface instance = new SomeInterface() {
      @Override
      public void someMethod() {
          // implementation body
      }
      // can also declare its OWN fields and additional methods
  };

Since Java 8, if the target type is a functional interface - exactly one abstract method - a lambda expression is almost always shorter and clearer than an anonymous class. Anonymous classes remain necessary for interfaces with more than one abstract method, for extending abstract classes (lambdas cannot extend anything), or when the implementation needs its own state.

1// File: AnonymousInnerClassDemo.java 2 3import java.util.ArrayList; 4import java.util.Comparator; 5import java.util.List; 6 7public class AnonymousInnerClassDemo { 8 9 interface NotificationHandler { 10 void onNotify(String message); 11 void onError(String error); // two abstract methods - cannot be a lambda 12 } 13 14 public static void main(String[] args) { 15 16 List<String> products = new ArrayList<>(List.of("Charger", "Earbuds", "Power Bank", "Cable")); 17 18 System.out.println("=== Anonymous inner class implementing Comparator ==="); 19 // Comparator has ONE abstract method - this COULD be a lambda, 20 // but is written as an anonymous inner class to show the mechanism 21 products.sort(new Comparator<String>() { 22 @Override 23 public int compare(String first, String second) { 24 return Integer.compare(first.length(), second.length()); 25 } 26 }); 27 products.forEach(p -> System.out.println(" " + p)); 28 29 System.out.println(); 30 31 System.out.println("=== Anonymous inner class implementing a multi-method interface ==="); 32 // NotificationHandler has TWO abstract methods - this CANNOT be a lambda 33 NotificationHandler handler = new NotificationHandler() { 34 private int notificationCount = 0; // anonymous classes CAN have their own fields 35 36 @Override 37 public void onNotify(String message) { 38 notificationCount++; 39 System.out.println(" [" + notificationCount + "] Notify: " + message); 40 } 41 42 @Override 43 public void onError(String error) { 44 System.out.println(" [ERROR] " + error); 45 } 46 }; 47 48 handler.onNotify("Order shipped"); 49 handler.onNotify("Order out for delivery"); 50 handler.onError("Payment retry failed"); 51 } 52}
Output:
=== Anonymous inner class implementing Comparator ===
  Cable
  Charger
  Earbuds
  Power Bank

=== Anonymous inner class implementing a multi-method interface ===
  [1] Notify: Order shipped
  [2] Notify: Order out for delivery
  [ERROR] Payment retry failed

How the Compiler Represents Nested Classes

Every nested class - regardless of which of the four forms it takes - compiles down to its own separate .class file. There is no special "nested" file format; the JVM has no concept of nesting at the class-loading level. Nesting is purely a source-code organizational feature that javac encodes through naming and a few synthetic members.

SOURCE FILE: SeatLayout.java contains all four nested-class forms

COMPILED OUTPUT - one .class file PER class, including nested ones:

  SeatLayout.class                  <- the outer class itself
  SeatLayout$SeatIterator.class     <- non-static member inner class
  SeatLayout$Builder.class          <- static nested class
  SeatLayout$1PriceMatcher.class    <- local class (numbered + named)
  SeatLayout$1.class                <- anonymous class (just a number)

EVERY non-static inner class carries a HIDDEN field:

  SeatLayout$SeatIterator
  +--------------------------------+
  | this$0 : SeatLayout              |  <- synthetic reference to the
  | currentRow : int                  |     SeatLayout instance that
  | currentCol : int                  |     created this iterator
  +--------------------------------+

  This hidden field is how grid, rows, and columns (private fields
  of SeatLayout) are reachable from inside SeatIterator without
  being passed in explicitly anywhere.

STATIC NESTED CLASSES HAVE NO this$0 FIELD:

  SeatLayout$Builder
  +--------------------------------+
  | rows : int                        |  <- only its OWN fields
  | columns : int                     |     no hidden outer reference
  | regularPrice : double             |     can be created with
  | premiumPrice : double             |     new SeatLayout.Builder()
  | premiumRows : int                  |     with zero SeatLayout
  +--------------------------------+     instances in existence

Two practical consequences follow from this. First, a non-static inner class instance keeps its outer instance reachable for as long as the inner instance itself is reachable - if you hand out an inner class instance to something long-lived (a listener registry, a cache, a thread), the outer object cannot be garbage collected until that reference is released too, even if nothing else points to the outer object. Second, nested classes - static or not - can read and write private members of their enclosing class, and the enclosing class can do the same to the nested class's private members. Java's private is enforced per top-level source file, not per individual .class file, so the compiler quietly generates package-private bridge methods where needed to make this cross-class private access work at the bytecode level.

Real-World Example - BookMyShow Seat Layout

A theatre seat layout for a booking platform like BookMyShow needs to: let calling code iterate over every seat, support a fluent way to construct a layout with different pricing tiers, filter seats by a caller-supplied condition, and sort seats by price for display. Each of those four needs maps naturally onto one of the four nested-class forms - which is rare for a single example, but seat layouts genuinely exercise all of them.

1// File: Seat.java 2 3public record Seat(String seatNumber, String category, double price, boolean booked) {}
1// File: SeatLayout.java 2 3import java.util.ArrayList; 4import java.util.Iterator; 5import java.util.List; 6import java.util.NoSuchElementException; 7 8public class SeatLayout implements Iterable<Seat> { 9 10 private final Seat[][] grid; 11 private final int rows; 12 private final int columns; 13 14 private SeatLayout(Seat[][] grid, int rows, int columns) { 15 this.grid = grid; 16 this.rows = rows; 17 this.columns = columns; 18 } 19 20 // FORM 1 - Member (non-static) inner class. 21 // SeatIterator needs to read 'grid', 'rows', and 'columns' - all 22 // private fields of THIS SeatLayout instance. Being non-static is 23 // what makes that direct access possible. 24 private class SeatIterator implements Iterator<Seat> { 25 private int currentRow = 0; 26 private int currentCol = 0; 27 28 @Override 29 public boolean hasNext() { 30 return currentRow < rows; 31 } 32 33 @Override 34 public Seat next() { 35 if (!hasNext()) { 36 throw new NoSuchElementException("No more seats in layout"); 37 } 38 Seat seat = grid[currentRow][currentCol]; 39 currentCol++; 40 if (currentCol == columns) { 41 currentCol = 0; 42 currentRow++; 43 } 44 return seat; 45 } 46 } 47 48 @Override 49 public Iterator<Seat> iterator() { 50 return new SeatIterator(); // implicitly tied to THIS SeatLayout instance 51 } 52 53 // FORM 2 - Static nested class. 54 // Builder constructs a SeatLayout but does not belong to any 55 // particular SeatLayout instance - it exists to CREATE one. 56 public static class Builder { 57 private int rows; 58 private int columns; 59 private double regularPrice = 150.0; 60 private double premiumPrice = 350.0; 61 private int premiumRows = 0; 62 63 public Builder rows(int rows) { 64 this.rows = rows; 65 return this; 66 } 67 68 public Builder columns(int columns) { 69 this.columns = columns; 70 return this; 71 } 72 73 public Builder premiumRows(int count, double price) { 74 this.premiumRows = count; 75 this.premiumPrice = price; 76 return this; 77 } 78 79 public SeatLayout build() { 80 Seat[][] grid = new Seat[rows][columns]; 81 for (int row = 0; row < rows; row++) { 82 for (int col = 0; col < columns; col++) { 83 String seatNumber = (char) ('A' + row) + String.valueOf(col + 1); 84 boolean isPremium = row < premiumRows; 85 grid[row][col] = new Seat( 86 seatNumber, 87 isPremium ? "PREMIUM" : "REGULAR", 88 isPremium ? premiumPrice : regularPrice, 89 false); 90 } 91 } 92 // Builder is a static nested class but can still call SeatLayout's 93 // private constructor - nested classes share access with their 94 // enclosing class regardless of the static modifier. 95 return new SeatLayout(grid, rows, columns); 96 } 97 } 98 99 // FORM 3 - Local inner class, used inside this instance method. 100 public List<Seat> findSeatsUnderBudget(double maxPrice) { 101 // Scoped to this method only - no other method can see PriceMatcher 102 class PriceMatcher { 103 boolean accepts(Seat seat) { 104 return seat.price() <= maxPrice; 105 } 106 } 107 108 PriceMatcher matcher = new PriceMatcher(); 109 List<Seat> result = new ArrayList<>(); 110 for (Seat seat : this) { // uses the SeatIterator from iterator() 111 if (matcher.accepts(seat)) { 112 result.add(seat); 113 } 114 } 115 return result; 116 } 117}
1// File: SeatLayoutDemo.java 2 3import java.util.ArrayList; 4import java.util.Comparator; 5import java.util.List; 6 7public class SeatLayoutDemo { 8 9 public static void main(String[] args) { 10 SeatLayout layout = new SeatLayout.Builder() 11 .rows(3) 12 .columns(4) 13 .premiumRows(1, 400.0) 14 .build(); 15 16 System.out.println("=== Iterating using the non-static inner class SeatIterator ==="); 17 for (Seat seat : layout) { 18 System.out.println(" " + seat.seatNumber() + " [" + seat.category() + "] Rs." + seat.price()); 19 } 20 21 System.out.println(); 22 23 System.out.println("=== Using a local inner class to filter by budget ==="); 24 List<Seat> budgetSeats = layout.findSeatsUnderBudget(200.0); 25 budgetSeats.forEach(seat -> System.out.println(" " + seat.seatNumber() + " Rs." + seat.price())); 26 27 System.out.println(); 28 29 System.out.println("=== Anonymous inner class - sorting with a custom Comparator ==="); 30 List<Seat> allSeats = new ArrayList<>(); 31 layout.forEach(allSeats::add); 32 33 // FORM 4 - Anonymous inner class implementing Comparator inline 34 allSeats.sort(new Comparator<Seat>() { 35 @Override 36 public int compare(Seat first, Seat second) { 37 return Double.compare(second.price(), first.price()); // descending 38 } 39 }); 40 allSeats.forEach(seat -> System.out.println(" " + seat.seatNumber() + " Rs." + seat.price())); 41 } 42}
Output:
=== Iterating using the non-static inner class SeatIterator ===
  A1 [PREMIUM] Rs.400.0
  A2 [PREMIUM] Rs.400.0
  A3 [PREMIUM] Rs.400.0
  A4 [PREMIUM] Rs.400.0
  B1 [REGULAR] Rs.150.0
  B2 [REGULAR] Rs.150.0
  B3 [REGULAR] Rs.150.0
  B4 [REGULAR] Rs.150.0
  C1 [REGULAR] Rs.150.0
  C2 [REGULAR] Rs.150.0
  C3 [REGULAR] Rs.150.0
  C4 [REGULAR] Rs.150.0

=== Using a local inner class to filter by budget ===
  B1 Rs.150.0
  B2 Rs.150.0
  B3 Rs.150.0
  B4 Rs.150.0
  C1 Rs.150.0
  C2 Rs.150.0
  C3 Rs.150.0
  C4 Rs.150.0

=== Anonymous inner class - sorting with a custom Comparator ===
  A1 Rs.400.0
  A2 Rs.400.0
  A3 Rs.400.0
  A4 Rs.400.0
  B1 Rs.150.0
  B2 Rs.150.0
  B3 Rs.150.0
  B4 Rs.150.0
  C1 Rs.150.0
  C2 Rs.150.0
  C3 Rs.150.0
  C4 Rs.150.0

Notice that SeatIterator never receives grid, rows, or columns through a constructor - it reads them directly because it is a member of SeatLayout. Builder, by contrast, never touches a SeatLayout instance at all until the final line of build() - it is entirely self-contained, which is exactly why it is static. The local PriceMatcher exists for the duration of one method call and nowhere else. And the anonymous Comparator is used exactly once, at the point it is needed, with no name anyone has to remember.

Inner Class vs Static Nested Class vs Local Class vs Anonymous Class

AspectMember Inner ClassStatic Nested ClassLocal ClassAnonymous Class
Declared with staticNoYesNo (not applicable)No (not applicable)
Needs an outer instance to createYes - outer.new Inner()No - new Outer.Nested()No - created directly in the methodNo - created at the point of use
Implicit reference to outer instanceYes (this$0)NoNo (but can capture enclosing instance if non-static method)No (but can capture enclosing instance if non-static method)
Can access outer's private instance fieldsDirectlyOnly via an instance passed to itDirectly, if declared in an instance methodDirectly, if declared in an instance method
Has its own nameYesYesYes (local to its method)No
Can declare a constructorYesYesYesNo (uses the supertype's constructor)
Visible outside the enclosing classYes, if public/protectedYes, if public/protectedNever - method-local onlyNever - exists only at its declaration point
Typical real useIterators, inner helper tied to instance stateBuilders, Node/Entry types, grouped constantsRare - a one-off algorithm needing local capturesInline Comparator, Runnable, event handlers

The pair that interview questions return to most often is the first two columns. The single deciding question is: does this nested class need to read or modify the state of a specific instance of the enclosing class? If yes, it is a member inner class. If the nested class is self-contained - it could exist and function with zero instances of the outer class anywhere in the program - it should be static.

Best Practices

Default to static for nested classes, and only drop it when you have a concrete reason. Every non-static nested class carries the hidden outer reference whether or not anything inside it actually uses it. If a nested class's methods never refer to the enclosing instance's fields or methods, making it static removes that unused reference, makes the class's actual dependencies explicit through its own constructor and fields, and avoids the memory-retention concern entirely.

Keep inner classes as private as the relationship allows. A SeatIterator that exists purely to implement iterator() has no reason to be visible outside SeatLayout - private class SeatIterator keeps it that way. A Builder, by contrast, is part of the public API and should be public static class Builder. The access modifier on a nested class should reflect who is actually meant to construct or reference it directly.

Prefer lambdas over anonymous classes for single-method functional interfaces, but do not force it. Comparator, Runnable, Callable, and similar single-method interfaces read more cleanly as lambdas in most cases. Reach for an anonymous class instead when the interface has more than one abstract method, when you are extending an abstract class rather than implementing an interface, or when the implementation genuinely needs its own fields - as NotificationHandler did with notificationCount in the example above.

Watch what holds onto inner and anonymous class instances. Because non-static inner and anonymous classes keep their enclosing instance alive, registering one with a long-lived structure - a static collection, an event bus, a cache - and never removing it is a quiet way to leak the entire outer object for the lifetime of that structure. If the nested class does not need outer access, making it static (or using a lambda, which does not capture this unless it explicitly references outer members) avoids this category of bug entirely.

Common Mistakes

Mistake 1 - Trying to Instantiate a Non-Static Inner Class From a Static Context

1// WRONG - 'Inner' is non-static, so it requires an enclosing Outer 2// instance. A static context (like main) has no implicit 'this' of 3// type Outer to supply. 4public class Outer { 5 class Inner { } 6 7 public static void main(String[] args) { 8 Inner inner = new Inner(); // COMPILE ERROR 9 } 10} 11 12// CORRECT - create an Outer instance first, then use outer.new Inner() 13public class OuterFixed { 14 class Inner { } 15 16 public static void main(String[] args) { 17 OuterFixed outer = new OuterFixed(); 18 Inner inner = outer.new Inner(); 19 } 20}

Mistake 2 - Making a Nested Class Non-Static When It Never Needs Outer Access

1// WRONG - StatusCode never reads anything from Order, yet being 2// non-static gives every StatusCode instance a hidden reference to 3// an Order instance it never uses 4public class Order { 5 class StatusCode { 6 private final int code; 7 8 StatusCode(int code) { 9 this.code = code; 10 } 11 12 String describe() { 13 return code == 200 ? "OK" : "FAILED"; // no Order access at all 14 } 15 } 16} 17 18// CORRECT - static nested class, no hidden outer reference, can be 19// created without any Order instance 20public class OrderFixed { 21 static class StatusCode { 22 private final int code; 23 24 StatusCode(int code) { 25 this.code = code; 26 } 27 28 String describe() { 29 return code == 200 ? "OK" : "FAILED"; 30 } 31 } 32}

Mistake 3 - Capturing a Local Variable That Gets Reassigned Afterward

1// WRONG - 'total' is reassigned AFTER PriceSummary is defined, so it 2// is not effectively final. The compiler rejects the capture inside 3// PriceSummary because the value could change after this point. 4void processPricesBroken(java.util.List<Double> prices) { 5 double total = 0; 6 7 class PriceSummary { 8 double getTotal() { 9 return total; // COMPILE ERROR - 'total' is not effectively final 10 } 11 } 12 13 total = 100.0; // this later reassignment is what breaks the capture 14} 15 16// CORRECT - finish computing the value, THEN define the local class 17// that reads it - no reassignment happens after the capture point 18void processPricesFixed(java.util.List<Double> prices) { 19 double total = prices.stream().mapToDouble(Double::doubleValue).sum(); 20 21 class PriceSummary { 22 double getTotal() { 23 return total; // 'total' is effectively final - never reassigned 24 } 25 } 26 27 System.out.println("Total: " + new PriceSummary().getTotal()); 28}

Mistake 4 - A Static Nested Class Reaching for an Instance Field That Does Not Belong to It

1// WRONG - Address is static, so it has no Employee instance to read 2// 'name' from. This is a frequent mistake when converting a member 3// inner class to static without checking its method bodies first. 4public class Employee { 5 private String name; 6 7 static class Address { 8 String describe() { 9 return "Employee: " + name; // COMPILE ERROR 10 } 11 } 12} 13 14// CORRECT - pass the needed value in explicitly, since Address has 15// no implicit connection to any particular Employee 16public class EmployeeFixed { 17 private String name; 18 19 static class Address { 20 String describe(String employeeName) { 21 return "Employee: " + employeeName; 22 } 23 } 24}

Interview Questions

Q1. What is the difference between a nested class and an inner class in Java?

"Nested class" is the umbrella term for any class declared inside another class - it includes both static and non-static forms. "Inner class" is the more specific term, referring only to non-static nested classes: member inner classes, local classes, and anonymous classes. A static nested class is a nested class but not technically an inner class, even though the two terms get used interchangeably in casual conversation. The practical distinction interviewers care about is what the static keyword changes: a non-static inner class carries a hidden reference to an instance of the enclosing class, while a static nested class does not.

Q2. Why can't you create an instance of a non-static inner class without an outer class instance?

Every non-static inner class instance has a synthetic field - this$0 in compiled bytecode - that holds a reference to the specific enclosing instance that created it, and that field is set during construction. Without an outer instance, there is nothing valid to put in that field. The syntax outerInstance.new InnerClass() is how you explicitly supply that outer instance from outside the enclosing class; from inside a non-static method of the enclosing class, new InnerClass() works directly because this (the implicit outer instance) is used automatically.

Q3. When would you choose a static nested class over a non-static inner class?

Choose static whenever the nested class's behavior does not depend on any particular instance of the enclosing class - it never needs to read or call that instance's fields or methods. Builder classes, simple data-holder types (an Address nested inside Employee, a Node nested inside a linked structure), and grouped constants or enums are typical cases. Making such a class static removes the unused hidden outer reference, which both clarifies the class's real dependencies - visible in its own constructor parameters - and avoids unintentionally keeping an enclosing instance alive through that reference.

Q4. Can a local inner class access local variables of the enclosing method - and what is the restriction?

Yes, but only variables that are effectively final - assigned once and never reassigned afterward, even if not declared with the final keyword explicitly. The reason is that the local variable lives on the enclosing method's stack frame, which may no longer exist by the time the local class instance is actually used - for example, if the instance is returned from the method or stored somewhere and used later. The compiler copies the variable's value into the local class instance at construction time; allowing the original to change afterward would make that copy silently inconsistent, so Java disallows the situation at compile time instead of letting it produce confusing runtime behavior.

Q5. How do anonymous inner classes relate to lambda expressions, and when do you still need an anonymous class?

Before Java 8, anonymous inner classes were the standard way to provide an inline implementation of an interface - Comparator, Runnable, and similar single-method types were commonly implemented this way at the point of use. Lambda expressions, introduced in Java 8, are a more concise syntax for exactly the same purpose when the target type is a functional interface with one abstract method. An anonymous class remains necessary when the interface has more than one abstract method (lambdas cannot implement multiple methods), when extending an abstract class rather than implementing an interface (lambdas cannot extend anything), or when the implementation needs its own instance fields beyond what a lambda's captured variables provide.

Q6. What is the this$0 field, and why does it matter for memory management in production code?

this$0 is a compiler-generated field present in every non-static inner class instance, holding a reference to the enclosing instance that created it - it is the mechanism that lets code inside the inner class read and call the outer instance's members without anything being passed explicitly. The production concern is reachability: as long as an inner class instance (or an anonymous class instance behaving the same way) is reachable from somewhere - a registered listener, an entry in a long-lived cache - its this$0 reference keeps the enclosing instance reachable too, even if nothing else points to it directly. An enclosing object that should have been eligible for garbage collection can be retained indefinitely this way, which is a well-known category of memory leak when listeners or callbacks implemented as inner or anonymous classes are registered with long-lived components and never explicitly unregistered.

FAQs

Can an inner class have static fields and methods?

A non-static (member) inner class can always declare static final constants - compile-time constant expressions - regardless of Java version, because such constants do not depend on any instance. Before Java 16, that was the only kind of static member a non-static inner class could have. Since Java 16, this restriction was relaxed, and non-static inner classes can declare any static members, including static methods and non-constant static fields.

Can a local inner class have an access modifier like public or private?

No. Local classes cannot be declared public, private, or protected, because they are not members of the enclosing class at all - they are local to the method, the same way a local variable is. Their visibility is automatically limited to the block in which they are declared, so an explicit access modifier would be meaningless.

Why do anonymous inner classes show up as ClassName dollar 1 in stack traces?

The compiler does not assign anonymous classes a readable name, so it generates one from the enclosing class plus a sequential number - the first anonymous class declared anywhere inside OrderService becomes OrderService$1, the second becomes OrderService$2, and so on, counted across the whole enclosing class regardless of which method each one appears in. This is why stack traces involving anonymous classes show numbered names instead of descriptive ones - one practical reason to prefer a small named (even private) inner class or a lambda when debuggability of that specific code path matters.

Can an interface contain a nested class?

Yes, and any nested type declared inside an interface - a class or another interface - is implicitly public and static, even without those keywords written out. This shows up throughout the JDK: Map.Entry is a nested interface inside Map, and it is implicitly static because an interface has no instances for a non-static nested type to be tied to.

What happens if the outer class and an inner class both have a field with the same name?

Inside the inner class, a plain reference to that field name resolves to the inner class's own field - it shadows the outer class's field of the same name. To explicitly reach the outer class's field in that situation, use OuterClassName.this.fieldName. This syntax works at any nesting depth - Outer.Middle.this.fieldName disambiguates which enclosing instance's field you mean when there are multiple levels of nesting involved.

Does using inner classes affect runtime performance?

No. Each nested class - member, static nested, local, or anonymous - compiles to its own separate .class file and is loaded by the JVM exactly like any top-level class; there is no special "nested class" mechanism at the bytecode or class-loading level that adds overhead. The only practical effect is more compiled files in the output directory and marginally more classes to load at startup, which is negligible for the vast majority of applications. The memory-retention concern from this$0 is a correctness and lifecycle issue, not a performance one in the traditional sense.

Summary

Inner classes are Java's answer to "this class only makes sense next to that other class." The static keyword is the fork in the road: drop it, and the nested class carries a hidden reference to a specific instance of its enclosing class, gaining direct access to that instance's private state - the right shape for iterators and similarly tightly-coupled helpers. Keep static, and the nested class is self-contained, behaving like any top-level class that simply lives in a more convenient namespace - the right shape for builders, node types, and grouped data.

Local classes and anonymous classes are the situational forms - local classes for a one-method algorithm that needs its own little type, anonymous classes for a one-time implementation written exactly where it is used, now often replaced by lambdas when the target is a single-method interface.

The question worth carrying forward from this topic: every time you reach for a non-static inner class, check whether it actually uses the outer instance for anything. If it does not, static is one keyword that removes a hidden reference, clarifies the class's real dependencies, and closes off an entire category of memory-retention bugs before they have a chance to appear.

What to Read Next