DEV Community

Mayur Bodhare
Mayur Bodhare

Posted on

When to Use a Nested Class in Java (and When Not To)

The first time I saw a class written inside another class, I thought it was a typo. Then I saw new Comparator<String>() { ... } with curly braces right after a constructor call, and I was sure my code editor was broken.

It wasn't. Java lets you put classes inside other classes, and it has several flavors of this: inner classes, static nested classes, local classes and anonymous classes. They look similar, but they behave quite differently. The biggest difference is something hidden that you never write: a secret link between the inner object and its outer object.

In this post, we'll look at each kind, see what the compiler really produces, and learn which one to choose. If you've read about abstract classes and interfaces, you'll recognize the building blocks.

Unless I show a full class, the snippets go inside a main method.

Why put a class inside another class?

A class that lives inside another class is called a nested class. People do it for a few good reasons:

  • Grouping. If a class is only useful to one other class, keep them together. A Node for a linked list belongs with the list.
  • Encapsulation. A nested class can use the private members of the class around it, and you can hide the nested class from the rest of the world.
  • Readability. The helper sits right next to the code that uses it.

Here is the family tree. Learn this shape first, because everything else hangs on it:

   Nested classes (classes written inside classes)
     |
     |-- static nested class            (has the keyword "static")
     |                                   no outer object needed
     |
     |-- inner classes                  (no "static")
           |                             always tied to an outer object
           |-- member inner class       (written in the class body)
           |-- local inner class        (written inside a method)
           |-- anonymous inner class    (no name, created with "new")
Enter fullscreen mode Exit fullscreen mode

The word "inner class" is used loosely in casual talk, but strictly speaking, inner means non-static.

Member inner class

A member inner class is a plain class declared inside another class, without static.

class Car {
    private String brand = "Tata";

    class Engine {
        void start() {
            System.out.println(brand + " engine started");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Look at start(). It uses brand, which is a private field of Car. An inner class can do that, because it's considered part of the outer class.

To create an inner object, you first need an outer object:

Car car = new Car();
Car.Engine engine = car.new Engine();
engine.start();
Enter fullscreen mode Exit fullscreen mode

Output:

Tata engine started
Enter fullscreen mode Exit fullscreen mode

The odd-looking car.new Engine() says: "create an Engine that belongs to this car." Every inner object belongs to one specific outer object. Two different cars would give you two engines that each use their own brand.

If you try to create it without an outer object:

Car.Engine e = new Car.Engine();
Enter fullscreen mode Exit fullscreen mode
an enclosing instance that contains Car.Engine is required
Enter fullscreen mode Exit fullscreen mode

Behind the scenes: the hidden reference

The compiler doesn't have a special "inner" feature in the JVM. It turns your inner class into a normal separate class with a funny name, and it quietly adds a hidden field that points to the outer object.

   Files created by javac:
     Car.class
     Car$Engine.class        <- the inner class gets its own file, named Outer$Inner

   At run time:

   Car object                          Engine object
  +----------------+                  +------------------------+
  | brand = "Tata" | <----------------| this$0  (hidden field) |
  +----------------+     reference    +------------------------+
Enter fullscreen mode Exit fullscreen mode

The field is usually named this$0, and the constructor secretly takes the outer object as an extra argument. That's how brand inside Engine really means this$0.brand. (Older Java versions also generated hidden helper methods so the inner class could reach private fields. Java 11 and newer handle this directly in the JVM.)

This hidden link has a side effect worth remembering: an inner object keeps its outer object alive. As long as the engine is reachable, the car can't be garbage collected, even if you no longer use the car directly.

Name clashes and Outer.this

What if the inner and outer classes both have a field with the same name? You can pick exactly the one you want:

class Outer {
    int x = 10;

    class Inner {
        int x = 20;

        void show() {
            int x = 30;
            System.out.println(x);
            System.out.println(this.x);
            System.out.println(Outer.this.x);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
new Outer().new Inner().show();
Enter fullscreen mode Exit fullscreen mode

Output:

30
20
10
Enter fullscreen mode Exit fullscreen mode
  • x alone is the closest one (the local variable).
  • this.x is the inner object's field.
  • Outer.this.x is the outer object's field. That's the hidden link, written out.

A small history note: older versions of Java didn't allow static members inside an inner class. Java 16 relaxed that, so newer code can have them.

Static nested class

Now add the word static. A static nested class is a nested class that is not tied to any outer object. It's basically an ordinary class that happens to live inside another class for organization.

class Library {
    private static int totalBooks = 0;

    static class Book {
        private final String title;

        Book(String title) {
            this.title = title;
            totalBooks++;
        }

        String getTitle() {
            return title;
        }
    }

    static int getTotalBooks() {
        return totalBooks;
    }
}
Enter fullscreen mode Exit fullscreen mode
Library.Book b1 = new Library.Book("Java Basics");
Library.Book b2 = new Library.Book("Clean Code");

System.out.println(b1.getTitle());
System.out.println(Library.getTotalBooks());
Enter fullscreen mode Exit fullscreen mode

Output:

Java Basics
2
Enter fullscreen mode Exit fullscreen mode

Notice the differences from the inner class:

  • We create it with new Library.Book(...). No Library object is needed.
  • It can use the outer class's static members, even the private ones (totalBooks).
  • It cannot use the outer class's instance members directly, because there's no outer object to read them from.

A common confusion: you can't make a top-level class static. The keyword only makes sense for a class nested inside another one. And static here doesn't mean that all its members are static. It only means "no hidden link to an outer object."

Behind the scenes

A static nested class is compiled to its own file too (Library$Book.class), but it has no this$0 field. It's lighter, and it can't accidentally keep an outer object alive.

   Member inner class                      Static nested class

   Engine object                           Book object
  +------------------------+              +--------------------+
  | this$0 -> Car object   |              | title              |
  | (hidden outer link)    |              | (no outer link)    |
  +------------------------+              +--------------------+
Enter fullscreen mode Exit fullscreen mode

You've already used static nested types without noticing. Map.Entry (the type of each item when you loop over a map) is a nested interface inside Map. And nested interfaces and enums are always static automatically.

Local inner class

A local inner class is declared inside a method. It exists only within that method, like a local variable.

public class Demo {
    public static void main(String[] args) {
        int base = 100;

        class Adder {
            int add(int n) {
                return base + n;
            }
        }

        System.out.println(new Adder().add(5));
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

105
Enter fullscreen mode Exit fullscreen mode

It's rarely needed, but it shows one important rule. A local (or anonymous) class can use local variables from the method only if they are final or effectively final, which means their value never changes after being set.

Why? Because the compiler copies the value into the class (it creates a hidden field like val$base). If the variable could change after the copy, the two would go out of sync, and Java prevents that confusion by banning the change.

Anonymous inner class

An anonymous inner class is a class with no name that you declare and create in a single expression. You use it when you need a one-time implementation of an interface or a one-time version of a class.

The syntax is new SomethingToExtendOrImplement() { ...body... }.

Implementing an interface

public class Main {
    interface Greeting {
        void sayHello(String name);
    }

    public static void main(String[] args) {
        Greeting g = new Greeting() {
            @Override
            public void sayHello(String name) {
                System.out.println("Hello, " + name);
            }
        };

        g.sayHello("Asha");
        System.out.println(g.getClass().getName());
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

Hello, Asha
Main$1
Enter fullscreen mode Exit fullscreen mode

Read it slowly. new Greeting() { ... } doesn't create a Greeting (you can't, it's an interface). It creates an object of a new unnamed class that implements Greeting, and the braces hold that class's body.

The last line reveals the secret: the compiler gave the nameless class a name anyway, Main$1. It numbers anonymous classes 1, 2, 3 and so on, and each one gets its own .class file.

Extending a class

You can also change the behavior of a normal class for just one object:

class Animal {
    void sound() {
        System.out.println("Some sound");
    }
}
Enter fullscreen mode Exit fullscreen mode
Animal cat = new Animal() {
    @Override
    void sound() {
        System.out.println("Meow");
    }
};

cat.sound();
Enter fullscreen mode Exit fullscreen mode

Output:

Meow
Enter fullscreen mode Exit fullscreen mode

No Cat class was ever written. If the parent's constructor takes arguments, you pass them inside the parentheses: new Animal("x") { ... }. An anonymous class can't have its own constructor, because it has no name to give one.

A real-life example

Before lambdas existed, this is how everyone sorted with a custom rule:

List<String> names = new ArrayList<>(List.of("Meera", "Jo", "Ravi"));

Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return Integer.compare(a.length(), b.length());
    }
});

System.out.println(names);
Enter fullscreen mode Exit fullscreen mode

Output:

[Jo, Ravi, Meera]
Enter fullscreen mode Exit fullscreen mode

The list is sorted by the length of each name. The same pattern was used for button click handlers, threads (new Runnable() { ... }) and many other callbacks.

Things to know

  • this is the anonymous object, not the outer one. To reach the outer object, write Outer.this.
  • Captured local variables must be effectively final. This won't compile:
int count = 0;
Runnable r = new Runnable() {
    public void run() {
        count++;
    }
};
Enter fullscreen mode Exit fullscreen mode
local variables referenced from an inner class must be final or effectively final
Enter fullscreen mode Exit fullscreen mode
  • An anonymous class isn't the same as an anonymous object. An anonymous object is new Dog().bark() (a normal class, but no variable). An anonymous class is a class with no name. They can even appear together, but they're separate ideas.

Look at all that typing

Look back at the Greeting and Comparator examples. For one tiny piece of logic (a single method), we wrote a new, a type name, braces, @Override, a full method signature and more braces. The real idea was a single line: "compare the lengths."

When the interface has exactly one abstract method, this ceremony is pure noise. Java 8 introduced a much shorter way to write it, called a lambda expression. That's the topic of the next post, and it builds directly on what you just learned.

Which one should I use?

My default rule is this: make a nested class static unless it truly needs the outer object. Static is lighter, and it avoids the hidden link that can keep objects alive.

  • Static nested class: for a helper that belongs with the outer class but doesn't need an outer instance. Examples: a Node in a linked list, a Builder, an Entry.
  • Member inner class: when the helper needs the outer object's data. A classic example is an Iterator for your own collection, which has to read the collection's fields.
  • Local inner class: only for a one-off helper inside a single method. It's uncommon.
  • Anonymous inner class: for a one-time implementation of an interface or abstract class, especially when the body is a few lines. For interfaces with a single method, prefer a lambda.
  • A normal top-level class: when many classes use it, when it's large or when you want to test it separately.

You can also control who sees a nested class. Unlike top-level classes, nested classes can be private, protected, default or public. A private static class Node tucked inside a collection is a very common way to hide a helper.

Common mistakes

  1. Creating an inner class without an outer object. new Car.Engine() fails with "an enclosing instance... is required." Use car.new Engine(), or make the class static.
  2. Making every nested class non-static by habit. It adds a hidden outer reference you may not need, and it can keep big outer objects in memory for longer than you expect. Add static unless you need the outer object.
  3. Changing a local variable inside a local or anonymous class. Captured variables must be effectively final. Use a field or a one-element array if you really need to change something.
  4. Mixing up this. Inside an inner or anonymous class, this is the inner object. Use Outer.this to reach the outer one.
  5. Writing long logic in an anonymous class. It becomes hard to read and can't be reused. Give it a proper name, and for a single-method interface, use a lambda.
  6. Misunderstanding the word "static." A top-level class can't be static, and a static nested class doesn't make its own members static. It only means "no outer object needed."

Quick recap

  • A nested class is a class written inside another class. Static nested classes have the static keyword. Inner classes (strictly speaking) don't.
  • A member inner class is tied to an outer object through a hidden reference (this$0), and it can use the outer's private members. Create it with outer.new Inner().
  • A static nested class has no outer object, can use the outer's static members, and is created with new Outer.Nested().
  • A local inner class lives inside a method. Local and anonymous classes can only use effectively final local variables.
  • An anonymous inner class is a nameless class created with new Type() { ... }. The compiler names it Outer$1, Outer$2 and so on.
  • Every nested class gets its own .class file, named Outer$Inner.class.
  • Default to static unless you need the outer object.

Try it yourself

Here's a small practice idea: build a simple MyStack class that stores items in a linked chain. Put a private static class Node inside it to hold each item. Then add an Iterator implemented as a member inner class that walks through the nodes, and use it in a for-each loop. You'll see why the iterator needs to be inner (it reads the stack's fields) and why the node can be static (it doesn't).

For a bonus experiment, compile any file with nested and anonymous classes, then list the output folder. Count the $ files and match each one to a class in your code.

If this helped, or if you spotted something I should fix, tell me in the comments. I'm still learning too.

Next in this series: functional interfaces and lambda expressions.

Top comments (0)