Nested and Inner Classes

Choose between static nested, inner, local and anonymous classes, and know why static nested is the safe default for builders and helpers.

Syntaxclass Outer { static class Nested { } // no outer instance class Inner { } // outer.new Inner() } Outer.Nested n = new Outer.Nested();

A class can be declared inside another class. This keeps a helper next to the only code that uses it and lets it share private access with its enclosing class. There are four kinds, and the differences matter.

KindDeclaredHas an outer instance?
Static nestedstatic class B inside a classNo
Innerclass B inside a class, no staticYes
LocalInside a method bodyYes, in an instance method
Anonymousnew Interface() { ... }Yes, in an instance method

Static nested: the default

A static nested class is an ordinary class that lives in another class's namespace, as in HttpRequest.Builder or Map.Entry. It needs no outer object. Use it for builders, small result types and helpers. Nested records, enums and interfaces are always static, even without the keyword.

Inner classes carry a hidden reference

An inner class instance holds a reference to the outer object that created it, which is how it can read the outer fields directly. Since JDK 18, javac drops that field when the inner class never uses the outer object, but the class still needs an outer instance to be created. You create one with outer.new Inner(). That hidden reference has costs. A long-lived inner object that uses outer state, such as a task submitted to an executor or a listener registered with a framework, keeps the whole outer object reachable, so a large service or cache cannot be garbage collected. JSON libraries also cannot create a non-static inner class from a request body, because they have no outer instance to give it. If the nested class does not read outer fields, mark it static.

Local and anonymous classes

A local class is declared inside a method and is visible only there. Local records are handy for an intermediate shape in a calculation. Local and anonymous classes can use local variables of the method only if those are final or effectively final.

Anonymous classes were the way to pass behaviour before Java 8. For an interface with one abstract method, a lambda is shorter and does not create a new scope for this. An anonymous class is still useful when you need state or more than one method, such as a small adapter.

A practical rule: start with a static nested class or a separate top-level class; use an inner class only when it genuinely works on the outer object's state.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Give a request or client configuration type a static nested Builder so callers set only the options they need.
  • Declare a local record inside a report method to hold parsed CSV rows for one calculation.
  • Replace an inner listener class with a static nested one so a registered callback no longer keeps a large service object alive.

More examples

An inner class reading outer state

Each Connection is tied to the Server that created it with api.new Connection(), and reads and updates that server's fields. As long as any connection object is reachable, its server is reachable too.

Example · java
public class Main {
    static class Server {
        private final String name;
        private int opened;

        Server(String name) {
            this.name = name;
        }

        class Connection {
            private final int number;

            Connection() {
                opened++;              // updates the outer object
                this.number = opened;
            }

            String describe() {
                return name + " connection #" + number;
            }
        }
    }

    public static void main(String[] args) {
        Server api = new Server("api");
        Server.Connection first = api.new Connection();
        Server.Connection second = api.new Connection();

        IO.println(first.describe());
        IO.println(second.describe());
        IO.println("opened: " + api.opened);
    }
}
// Output:
// api connection #1
// api connection #2
// opened: 2

Anonymous class versus lambda

The anonymous class and the lambda both implement Comparator; the lambda is the modern form for any single-method interface. Library factories such as Comparator.comparingInt(String::length) remove the need for either.

Example · java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> routes = new ArrayList<>(List.of("/orders", "/a", "/users/me"));

        routes.sort(new Comparator<String>() {
            @Override
            public int compare(String x, String y) {
                return Integer.compare(x.length(), y.length());
            }
        });
        IO.println(routes);

        routes.sort((x, y) -> Integer.compare(y.length(), x.length()));
        IO.println(routes);

        routes.sort(Comparator.comparingInt(String::length));
        IO.println(routes);
    }
}
// Output:
// [/a, /orders, /users/me]
// [/users/me, /orders, /a]
// [/a, /orders, /users/me]

A local record inside a method

The Line record exists only inside cheapest(), so it does not clutter the rest of the code base. Local records are implicitly static and cannot capture the method's local variables, which keeps them simple.

Example · java
import java.util.List;

public class Main {
    static String cheapest(List<String> lines) {
        record Line(String sku, int cents) { }

        Line best = null;
        for (String raw : lines) {
            String[] parts = raw.split(",");
            Line line = new Line(parts[0], Integer.parseInt(parts[1]));
            if (best == null || line.cents() < best.cents()) {
                best = line;
            }
        }
        return best == null ? "none" : best.sku() + " at " + best.cents();
    }

    public static void main(String[] args) {
        IO.println(cheapest(List.of("mug,899", "pen,250", "cap,1200")));
        IO.println(cheapest(List.of()));
    }
}
// Output:
// pen at 250
// none

Discussion

  • Be the first to comment on this lesson.