Nested, Inner and Anonymous Classes in Java

A class declared inside another can be static, inner, local or anonymous, and the difference is mainly what each one can see.

The four kinds

KindDeclaredHolds outer instanceTypical use
Static nestedIn a class, with staticNoA helper tied to the outer type
Inner (non static)In a class, without staticYesNeeds the outer object state
LocalInside a methodYes, if in an instance methodUsed only in that method
AnonymousAt the point of useYes, if in an instance contextA one off implementation

Static nested class

public class Catalogue {

    private final List<Entry> entries = new ArrayList<>();

    public void add(String title, double price) {
        entries.add(new Entry(title, price));
    }

    public static class Entry {          // no link to any Catalogue instance
        private final String title;
        private final double price;

        Entry(String title, double price) {
            this.title = title;
            this.price = price;
        }
    }
}
Catalogue.Entry entry = new Catalogue.Entry("Java notes", 250);

It is an ordinary class that happens to be scoped inside another, which signals that the two belong together. This is the form to reach for by default.

Inner class

public class Library {

    private final String name;

    public Library(String name) {
        this.name = name;
    }

    public class Membership {            // each instance belongs to one Library
        private final String holder;

        public Membership(String holder) {
            this.holder = holder;
        }

        public String describe() {
            return holder + " at " + name;    // reads the outer field directly
        }
    }
}
Library library = new Library("City Library");
Library.Membership card = library.new Membership("Arun");   // note the syntax
System.out.println(card.describe());   // Arun at City Library
An inner class instance keeps a hidden reference to its enclosing object. That is convenient, but it also means the outer object cannot be collected while the inner one is alive, which is a classic source of memory retention. Use static unless the outer state is genuinely needed.

Reaching the outer instance

public class Outer {
    private String label = "outer";

    class Inner {
        private String label = "inner";

        void show() {
            System.out.println(label);              // inner
            System.out.println(this.label);         // inner
            System.out.println(Outer.this.label);   // outer
        }
    }
}

Local class

public List<String> report(List<String> rows, String prefix) {

    class Formatter {                     // visible only inside this method
        String format(String row) {
            return prefix + ": " + row;   // captures an effectively final local
        }
    }

    Formatter formatter = new Formatter();
    return rows.stream().map(formatter::format).toList();
}

A local class can capture local variables that are final or effectively final. It is rarely needed now, because a lambda usually says the same thing more briefly.

Anonymous class

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

The class has no name, is declared and instantiated in one expression, and can implement an interface or extend a class. Before Java 8 this was the standard way to pass behaviour.

Anonymous class compared with lambda

Comparator<String> anonymous = new Comparator<>() {
    @Override public int compare(String a, String b) {
        return a.compareTo(b);
    }
};

Comparator<String> lambda = (a, b) -> a.compareTo(b);
Comparator<String> shorter = Comparator.naturalOrder();
AspectAnonymous classLambda
Abstract methodsAny numberExactly one
Can extend a classYesNo, interfaces only
Own stateYes, may declare fieldsNo
this refers toThe anonymous instanceThe enclosing instance
Class fileA separate one is generatedLinked at runtime

Use a lambda for a single method interface. Keep anonymous classes for the cases a lambda cannot express: extending a class, adding state, or implementing more than one method.

A practical use of nesting

public final class SearchResult {

    private final List<Match> matches;

    private SearchResult(List<Match> matches) {
        this.matches = List.copyOf(matches);
    }

    public record Match(long noteId, String snippet, double score) { }

    public static SearchResult of(List<Match> matches) {
        return new SearchResult(matches);
    }
}

Match has no meaning outside a search result, so nesting it states that and keeps the package tidy.

Common mistakes

  • Using an inner class where a static nested one would do, and retaining the outer object unnecessarily.
  • Forgetting the outer.new Inner() syntax and trying new Outer.Inner().
  • Expecting this inside an anonymous class to mean the enclosing object.
  • Declaring an inner class inside a long lived object such as a listener registry, and leaking the whole outer graph.
  • Writing an anonymous class for a single method interface where a lambda is clearer.

Best practices

  • Prefer static nested classes; add the outer link only when the state is needed.
  • Nest a class when it is meaningless outside its enclosing type, and keep it private when possible.
  • Use a record for a nested data carrier.
  • Reach for a lambda first, and an anonymous class only when a lambda cannot express it.

Practice

  1. Convert an inner class to a static nested class and list what had to change.
  2. Why does new Library.Membership("Arun") fail to compile?
  3. Print both the inner and outer field of the same name from inside an inner class.
  4. Rewrite an anonymous Runnable as a lambda and explain what this now refers to.
  5. Describe a realistic case where an inner class causes a memory leak.

Conclusion

Nest a class when it belongs to its enclosing type. Make it static unless the outer instance is genuinely required, and let lambdas replace the anonymous classes that only implement one method.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.