Nested Classes in Java

In object-oriented languages, a nested or inner class is a class that’s completely declared within another class.
This allows us to combine classes that are logically bound together, to increase encapsulation, for more concise and maintainable code.
Here’s a quick, non-deep-dive overview of the 4 types of nested classes.
Table of Contents
Static Nested Classes
A nested class is defined like any other class:
package com.mypackage;
public class Outer {
public static class Nested {
// ...
}
}
Like a static
member, a static
nested class is bound to the class itself, and not an instance of it.
This means we can instantiate it without creating an intermediate instance of Outer
first:
Outer.Nested instance = new Outer.Nested();
It behaves just like any other class, and follows the same rules:
- All access modifiers are supported
- Both
static
and non-static
members can be defined - Can’t access non-
static
members of its enclosing class
For simpler usage we can even import
the nested class to drop the outer class prefix:
import com.mypackage.Outer.Nested;
Nested instance = new Nested();
When to Use
static
nested classes behave just like any other top-level class, and should be treated as such. The main advantage is packaging convenience.
Non-Static Nested Classes / Inner Classes
Non-static
nested classes are also known as inner classes:
public class Outer {
public class Nested {
// ...
}
}
Instead of being associated with the Outer
class type, the inner Nested
class is bound to instances of its enclosing class.
Thanks to this kind of relationship, it has access to all members of the enclosing class, not just the static
members. But the inner class can’t define any static
members itself, though.
To instantiate the inner class, we now need an instance of its enclosing class:
Outer outer = new Outer();
Outer.Nested nested = outer.new Nested();
// THIS IS NOT ALLOWED!
Outer.Nested nested = new Outer.Nested();
There’s no longer just a single type, that just happens to be nested under another class. An inner class is tightly bound to the actual instance of its enclosing class, and can’t live on its own anymore.
Serialization
Just because the enclosing class might be serializable, it doesn’t make the nested class automatically serializable, too.
As with any other members, we must ensure that they implement java.io.Serializable
as well.
Or we might end up with a java.io.NotSerializableException
.
When to Use
Inner classes have the advantage of having a deeper connection to their enclosing class, including full access to all of its members. But this connection can lead to non-obvious memory retention. The enclosing class can’t be garbage-collected until the nested instance can, too.
Resources
Inner Classes and Enclosing Instances (JLS)
Local Classes
Local classes are a specialized form of inner classes.
We can define a local class in any kind of block (e.g., methods):
public class MyClass {
public void run() {
class MyLocalClass {
// ...
}
}
}
Just like an inner class, we can access all members of the enclosing class. But we can’t provide an access modifier to the class, because it can only be used locally.
When to Use
We can achieve the same behavior with an inner class. But it wouldn’t bind the logic as strongly to a specific block as a local class does.
It’s a great tool for better grouping logic together, and with local classes, we can use the smallest footprint possible.
Resources
Local Class Declarations (JLS)
Anonymous Classes
Anonymous classes are not about defining a nested class, but creating a new class by instantiating a pre-existing type:
Runnable runnable = new Runnable() {
@Override
public void run() {
// ...
}
};
We just created a new class based on the interface Runnable
, and because it doesn’t have a name, it’s anonymous.
Not only interfaces can be used for creating an anonymous class. We can also extend other non-final
classes:
List<String> customStringBuilder = new ArrayList<>(10) {
public boolean add(String value) {
System.out.println("Adding value: " + value);
return super.add(value);
}
// ...
};
A specialized List<String>
implementation without the need to create a separate class altogether, neat!
The creation syntax always follows the same structure:
new <<Type>>(<<constructor arguments>>) {
// declarations / overrides
};
Anonymous class declarations are expressions and must be a part of a statement, either in a block or as a member declaration itself.
Anonymous Classes Vs. Lambdas
With the advent of lambda expressions we finally got a simpler way of implementing types on the spot:
// ANONYMOUS CLASS
Predicate<String> anonymous = new Predicate<String>() {
@Override
public boolean test(String t) {
return t != null;
}
};
// LAMBDA EXPRESSION
Predicate<String> lambda = (input) -> input != null;
The functionally of both predicates is identical, so we might think that lambdas are just syntactic sugar for anonymous classes. The generated ByteCode differs, revealing that it might behave the same way, but isn’t done the exactly in the same manner:
// ANONYMOUS CLASS
0: new #2 // class Anonymous$1
3: dup
4: invokespecial #3 // Method Anonymous$1."<init>":()V
7: astore_1
8: return
// LAMBDA
0: invokedynamic #2, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
5: astore_1
6: return
The lambda uses the opcode invokedynamic
, which allows for a more dynamic method invocation by the JVM.
When to Use
Anonymous classes are great for small, specific implementations on the spot. Even though a lambda might be sufficient.
Due to their simplicity, there are also multiple downsides compared to local or inner classes:
- Not having a name can make stacktraces harder to follow
- Only a single type can be used, no additional interfaces etc.
- More complex syntax
Resources
- Anonymous Class Declaration (JLS)
- Anonymous classes in the VM (Oracle)
Shadowing
In software-development, shadowing is the re-declaration of a member in a deeper scope. This means we can re-use variable names in nested classes and also access the shadowed member by prefixing the call:
public class Outer {
String stringVal = "Outer class";
public class Nested {
String stringVal = "Nested Class";
public void run() {
System.out.println("Nested stringVal = " + this.stringVal);
System.out.println("Outer stringVal = " + Outer.this.stringVal);
}
}
}
Conclusion
It’s great that we can logically group classes together, or create anonymous instances on the spot. But we need to carefully decide which type of nested class we actually want and need.
Especially for functional interfaces a lambda might be a more concise solution.
Additional Resources
- The Java Tutorials: Nested Classes (Oracle)
- Inner class (Wikipedia)
- Nested Classes in Java (Baeldung)
- Java -Inner Classes (Tutorialspoint)