Module 6 · Inheritance, Polymorphism & Design ⏱ 19 min

Nested Classes and Lambdas Intro

By the end of this lesson you will be able to:
  • Distinguish static nested classes from inner classes and predict which outer members each can access
  • Write a lambda expression that implements a functional interface
  • Predict capture behaviour and what `this` refers to inside a lambda

Java lets you define a class inside another class. A nested class keeps helper logic close to the class that uses it, so related code travels together instead of polluting the top-level namespace. If you have ever opened a file and found dozens of small helper classes only used by one dominant class, nested classes let you move those helpers inside and keep the package clean.

There are two broad families. A static nested class is just a class parked inside another for packaging — it behaves like any top-level class, but its name is Outer.Nested. An inner class is non-static and each instance is permanently tied to an instance of the outer class, giving it direct access to the outer object's fields and methods.

Understanding the difference matters because the wrong choice leaks memory (an inner class holds a hidden reference to its outer instance) or produces compiler errors when you try to reach an instance field from a static context.

flowchart TD
  A["Outer class"] --> B["static class Nested<br/>no outer instance needed"]
  A --> C["class Inner<br/>needs outer instance"]
  style B fill:#3776ab,color:#fff
  style C fill:#b45309,color:#fff
Static nested classes need no outer instance; inner classes are bound to one.

A static nested class is declared with the static keyword inside its enclosing class. Because it is static, it does not receive a hidden reference to an outer instance. That means it can touch the outer class's static fields and methods directly, but it cannot read instance fields — there is no this of the outer class to read from.

You instantiate it with new Outer.Nested(), even if no Outer instance exists. That independence makes static nested classes useful for small utility types, builders, or data carriers that are only meaningful in the context of the outer class but do not need to interact with a particular outer object. If the nested class does not need outer state, making it static is the safer default.

Main.java — a static nested class reading an outer static field. Real, compiling Java.
public class Main {
    static class Config {
        static int timeout = 30;

        static class Validator {
            void check() {
                System.out.println("timeout is " + timeout);
            }
        }
    }

    public static void main(String[] args) {
        Config.Validator v = new Config.Validator();
        v.check();
    }
}

An inner class drops the static keyword. Every inner object is created through an outer object — either with outer.new Inner() or implicitly from within an instance method of the outer class. The compiler secretly stores a back-reference to the outer instance, which is why the inner object can read private fields of the outer object without any accessor methods.

That hidden reference is powerful, but it is also a cost: the inner object cannot exist without the outer object, and it keeps the outer object alive as long as the inner object is reachable. If you store an inner object in a static list, you may accidentally prevent its outer object from being garbage-collected. For short-lived helpers that do not need outer state, prefer static. If you need outer state, consider whether a simple method argument would be cleaner than an inner class.

flowchart LR
  O["Outer instance<br/>value = 42"] -->|"hidden reference"| I["Inner instance"]
  I -->|"reads value"| O
  style O fill:#3776ab,color:#fff
  style I fill:#b45309,color:#fff
An inner object carries a hidden reference back to the outer instance that created it.
Main.java — an inner class reading an outer instance field. Real, compiling Java.
public class Main {
    static class Room {
        int temperature = 22;

        class Sensor {
            void read() {
                System.out.println(temperature);
            }
        }
    }

    public static void main(String[] args) {
        Room r = new Room();
        Room.Sensor s = r.new Sensor();
        s.read();
    }
}

Java 8 introduced lambda expressions to replace the boilerplate of anonymous inner classes for single-method interfaces. A functional interface is any interface with exactly one abstract method — Runnable, Comparator, and many others in java.util.function fit this rule. The @FunctionalInterface annotation is optional, but when present it tells the compiler to reject any accidental second abstract method.

The lambda syntax is (parameters) -> expression or (parameters) -> { statements; }. The compiler matches the lambda against the expected functional interface and synthesises the method for you. What used to take six lines of anonymous class now takes one. The type of a lambda is always the functional interface it is assigned to — there is no standalone 'lambda type'. You cannot assign a lambda to Object; you must assign it to a functional interface type.

flowchart TD
  L["Lambda inside Outer"] -->|"this refers to"| O["Outer instance"]
  A["Anonymous class"] -->|"this refers to"| S["Anonymous instance itself"]
  style O fill:#3776ab,color:#fff
  style S fill:#b45309,color:#fff
Inside a lambda, `this` means the enclosing class; inside an anonymous class, `this` means the anonymous object.
Main.java — a lambda reading the enclosing instance via `this`. Real, compiling Java.
public class Main {
    String name = "Main";

    interface Greeter {
        void greet();
    }

    void run() {
        Greeter g = () -> System.out.println(this.name);
        g.greet();
    }

    public static void main(String[] args) {
        new Main().run();
    }
}

Before lambdas, the closest tool was an anonymous classnew Greeter() { public void greet() { ... } }. Anonymous classes are inner classes, so their this refers to the anonymous instance itself. To reach the outer class from inside an anonymous class you must write Outer.this.name.

Lambdas do not introduce a new scope for this. Inside a lambda, this refers to the enclosing instance — the object whose method contains the lambda. That is usually what you wanted, but it surprises developers who are used to anonymous classes. If you need the lambda to behave like an anonymous class and introduce its own this, you must use an anonymous class instead.

Method references

Method references are a shorthand for lambdas that simply call an existing method. System.out::println is a Consumer<String> that calls println on each item. String::length is a Function<String, Integer> that returns the length. They compile to the same bytecode as the equivalent lambda, but they read more declaratively.

You cannot use a method reference when you need to transform the arguments — that is when the full lambda syntax is necessary.

Exercise

What does this program print? Read it carefully and type the exact output (one line).

public class Main {
    static class Outer {
        static int x = 7;
        static class Inner {
            void show() {
                System.out.println(x);
            }
        }
    }
    public static void main(String[] args) {
        Outer.Inner i = new Outer.Inner();
        i.show();
    }
}
Exercise

What does this program print? Read it carefully and type the exact output (one line).

public class Main {
    static class Message {
        String text = "Hello";
        class Formatter {
            void print() {
                System.out.println(text);
            }
        }
    }
    public static void main(String[] args) {
        Message m = new Message();
        Message.Formatter f = m.new Formatter();
        f.print();
    }
}
Exercise

Which of the following interfaces can be implemented with a lambda expression?

Exercise

What does this program print? Read it carefully and type the exact output (one line).

public class Main {
    int value = 2;
    interface Doubler { int make(); }
    Doubler create() {
        return () -> value * 2;
    }
    public static void main(String[] args) {
        Main m = new Main();
        Doubler d = m.create();
        m.value = 5;
        System.out.println(d.make());
    }
}
Exercise

What happens if you write a lambda that captures a local variable, then try to change that variable later in the method?

Recap

  • Static nested classes belong to the outer class, not an instance — use new Outer.Nested() and access only static outer members.
  • Inner classes hold a hidden reference to an outer instance and can read all its fields, including private ones.
  • A lambda implements a functional interface — an interface with exactly one abstract method.
  • Inside a lambda, this refers to the enclosing instance, not the lambda itself.
  • Method references like System.out::println are compact lambdas that call an existing method.
  • Lambdas capture effectively final locals by value, but read instance fields live through this.

Checkpoint quiz

What can a static nested class access directly from its enclosing class?

Inside a lambda defined in an instance method, what does this refer to?

Go deeper — technical resources