Nested Classes in Java

 · 6 min
Alberto Triano on Unsplash

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.


Static Nested Classes

A nested class is defined like any other class:

java
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:

java
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:

java
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:

java
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:

java
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):

java
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:

java
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:

java
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:

java
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:

java
// 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


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:

java
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.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, Korean, and soon, Chinese.

Additional Resources