Back to Blog
Java

Java Inner Class: Syntax, Types, and When to Use Them

java inner class: Learn the syntax and behavior of Java inner classes, including member, local, anonymous, and static nested types, with practical usage guidance.

inner classesnested classesanonymous classesJava syntaxobject-oriented design
Diagram showing a Java class containing a nested inner class with an arrow indicating access to the outer class instance.

java inner class requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you declare a class inside another class in Java, you create an inner class. The most common use is to model a component that belongs exclusively to its enclosing class, such as a node inside a linked list or a comparator for sorting. Inner classes can access the private members of the outer class, which keeps related logic in one place and reduces boilerplate. This article explains the four kinds of nested types in Java, how they differ, and when each one is appropriate.

The Basic Syntax of a Java Inner Class

A Java inner class is declared inside the body of another class. The simplest form is a member inner class, which is written at the same level as fields and methods:

public class Outer { private int value; class Inner { void printValue() { System.out.println(value); } } }

The Inner class is a non-static member of Outer. It can read value directly because it has an implicit reference to the enclosing Outer instance. To create an Inner object, you first need an Outer instance:

Outer outer = new Outer(); Outer.Inner inner = outer.new Inner();

The syntax outer.new Inner() is unusual but necessary because each inner object is tied to a specific outer object. This relationship is the key difference between inner classes and static nested classes.

Member Inner Classes: Accessing the Outer Instance

A member inner class is associated with an instance of the outer class. The compiler adds a hidden field that holds a reference to the outer object, and this reference is initialized when the inner class is instantiated. Because of this, the inner class can access all fields and methods of the outer class, including private ones.

This behavior is useful when you need a helper class that operates on the state of the enclosing object. For example, an Iterator implementation that walks over a custom collection often benefits from being a member inner class:

public class CustomList { private Object[] items; class ListIterator implements Iterator<Object> { private int index; public boolean hasNext() { return index < items.length; } public Object next() { return items[index++]; } } }

The iterator can directly access items without any getter. This keeps the iterator tightly coupled to the list's internal representation, which is acceptable when the iterator is not meant to be reused outside the list.

One consequence of the implicit outer reference is that a member inner class cannot be instantiated without an outer instance. This can be a disadvantage if you need a standalone object. It also affects memory: the inner object holds a reference to the outer object, which can prevent garbage collection if the inner object outlives the outer object in a long-lived data structure.

Local Inner Classes and Scope

A local inner class is defined inside a method body. It is scoped to that method and can access local variables and parameters, provided they are effectively final. This is useful when you need a small helper class that is only relevant within one method.

public void process() { int limit = 10; class Validator { boolean isValid(int number) { return number < limit; } } Validator v = new Validator(); if (v.isValid(42)) { // ... } }

The local class can access limit because it is effectively final. If you try to modify limit after the class declaration, the compiler will reject it. This restriction exists because the local class captures the value at the point of instantiation, and the Java language specification requires that captured variables not change.

Local inner classes are often used when you need a one-off implementation that is too complex for an anonymous class but not worth promoting to a top-level class. They keep the code close to where it is used, improving readability when the logic is not reused elsewhere.

Anonymous Inner Classes for One-Off Implementations

An anonymous inner class is a local class without a name. It is declared and instantiated in a single expression, typically to provide an implementation of an interface or an extension of a class. This is common when passing a callback or a comparator.

List<String> names = Arrays.asList("b", "a", "c"); Collections.sort(names, new Comparator<String>() { public int compare(String s1, String s2) { return s1.compareTo(s2); } });

The anonymous class implements Comparator<String> and overrides compare. It can access effectively final local variables from the enclosing method, just like a local inner class. Anonymous classes are concise, but they have limitations: you cannot define constructors, and you cannot have multiple methods if the interface requires more than one (unless you use a lambda in modern Java).

For functional interfaces, a lambda expression is usually a better choice because it is more readable and avoids the verbose anonymous class syntax. However, anonymous classes are still useful when you need to extend a class with additional fields or when the interface has multiple abstract methods.

Static Nested Classes vs. Inner Classes

A static nested class is declared with the static keyword. Unlike a member inner class, it does not have a reference to an outer instance. This means it cannot access instance fields of the outer class directly; it can only access static members.

public class Outer { private static int staticValue; private int instanceValue; static class StaticNested { void printStatic() { System.out.println(staticValue); } // Cannot access instanceValue without an Outer reference } }

To instantiate a static nested class, you do not need an outer instance:

Outer.StaticNested nested = new Outer.StaticNested();

Static nested classes are often used for grouping related classes that do not need access to the outer instance. For example, a Builder class inside a domain object is typically static because it only needs the constructor parameters, not the outer object's state.

The key distinction is the implicit outer reference. A non-static inner class holds that reference; a static nested class does not. This affects memory, instantiation, and access rules. If you do not need to access instance members of the outer class, prefer a static nested class to avoid the hidden reference and the associated memory overhead.

Instantiation and Memory Behavior

Creating a member inner class requires an outer instance, and the inner object holds a reference to that outer instance. This reference is a strong reference, which means the outer object cannot be garbage collected as long as the inner object is reachable. In long-lived applications, this can lead to memory leaks if inner objects are stored in collections while the outer object is no longer needed.

Consider a cache that stores inner class instances. If the cache outlives the outer object, the outer object remains alive because the inner objects reference it. This is a common source of memory pressure in Java applications. Static nested classes do not have this problem because they do not hold an outer reference.

For local and anonymous inner classes, the same rule applies: they capture the outer instance if they access any instance member. If they only use local variables, they do not need an outer reference, but the compiler may still generate one if the class is non-static and the enclosing method is in an instance context.

When designing your code, consider whether the inner class really needs access to the outer instance. If not, make it static. This reduces memory usage and makes the class easier to test in isolation.

Choosing the Right Nested Type

The decision between the four nested types depends on what the class needs to do:

  • Use a member inner class when the helper class must access the outer instance's fields and methods, and when the helper is conceptually tied to a specific outer object.
  • Use a local inner class when the helper is only needed inside a single method and you want to keep it close to the logic.
  • Use an anonymous inner class for a short, one-off implementation of an interface or a small subclass, especially when a lambda is not suitable.
  • Use a static nested class when the helper does not need access to the outer instance, or when you want to avoid the implicit outer reference for memory or design reasons.

A common mistake is making every nested class non-static out of habit. If you never use the outer instance's fields, the extra reference is unnecessary. For example, a Builder that constructs an object from parameters should be static. A Node class inside a linked list might be static if it only stores data and a next pointer, because it does not need to access the list's head or size.

The choice also affects testability. A static nested class can be instantiated without an outer object, which simplifies unit testing. A non-static inner class requires an outer instance, which can complicate test setup. If the inner class is complex, consider extracting it to a top-level class or making it static to improve separation of concerns.

Finally, remember that anonymous and local inner classes are subject to the effectively final rule for captured variables. This is a compile-time constraint that can surprise developers who try to modify a variable inside a callback. Understanding this rule helps you avoid errors when using inner classes in event handlers or asynchronous code.

java inner class: Practical Usage and Code Examples | RYUSLOG DEV