Robust Oop

Nested & Anonymous Classes; Lambdas

Classes can nest inside classes (static nested, or inner with a link to the enclosing instance); an anonymous class implements an interface inline; and a lambda is a compact anonymous implementation of a functional interface — one abstract method — turning behavior into a value you can pass and store. Method references shorten lambdas that just call a method. The bridge from objects to functional style, shown with verified output.

Suggest an edit

Nested & Anonymous Classes; Lambdas — Behavior as a Value

So far a method's behavior has been fixed where it's written. This chapter makes behavior something you can pass around. The path there runs through Java's ways of defining a class in a smaller scope: nested classes (a class inside a class), anonymous classes (an unnamed class implementing an interface right where it's used), and finally lambdas — a compact, anonymous implementation of a functional interface (an interface with exactly one abstract method). A lambda turns "what to do" into a value you can store in a variable, hand to a method, and call later. Method references shorten the common case of a lambda that just calls an existing method. Together these are the bridge from object-oriented to functional Java — and the foundation for streams in the next tier.

💡 The core idea.

  • These turn behavior into a value you can pass, store, and call later.
  • Nested and anonymous classes define a class in a smaller scope.
  • A lambda is a compact implementation of a functional interface (one abstract method).
  • Method references shorten a lambda that just calls a method — the bridge to functional Java.

This builds on methods and interfaces. Every output below was produced by compiling and running the code.

📘 How to read the Intuition boxes. Each one is built in three moves:

  1. The mechanism — what the compiler and the JVM are actually doing.
  2. A concrete bite — a specific, runnable failure (often a real compiler error), shown so the trap is visible.
  3. The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.

Table of contents

  1. Nested classes
  2. Anonymous classes
  3. Lambdas
  4. Method references
  5. Mental-model summary
  6. Gotcha checklist

1. Nested classes

A class can be declared inside another. A static nested class is just a class scoped to its enclosing one. A non-static inner class additionally holds a hidden link to an enclosing instance, so it can read that instance's fields.

Output:

15
15

Analysis. StaticNested needs no Outer instance — new Outer.StaticNested() — and can't see x. Inner is different: it's created from an Outer instance (outer.new Inner()) and can read that instance's x (so addX(5) is 5 + 10 = 15). The static keyword on a nested class is exactly the same idea as on a field or method: static means "no enclosing instance," non-static means "tied to one."

Intuition. Mechanism. An inner (non-static) class instance carries a reference to its enclosing instance, which is how it reaches x. A static nested class carries no such reference — it's a top-level class that merely lives in another's namespace.

Concrete bite. The practical rule: default a nested class to static unless it genuinely needs the enclosing instance. A non-static inner class keeps its outer object alive (a hidden reference), which can cause surprising memory retention; static avoids that link entirely.

💡 Earned rule. Nest a helper class inside the class it serves to keep it scoped and private, and make it static unless it must access enclosing instance state. The cost of a non-static inner class is the hidden outer reference (memory and coupling); the benefit of nesting is locality — the helper lives exactly where it's used, not as a separate top-level file.


2. Anonymous classes

To implement an interface just once, right where you need it, you can write an anonymous classnew Interface() { ... } — which defines and instantiates an unnamed implementing class in one expression.

Output:

Hello, Ada

Analysis. new Greeter() { ... } created an object of an unnamed class that implements Greeter, supplying greet inline. We never declared a named class — the implementation exists only as this one object. This is how Java passed behavior before lambdas: wrap it in an anonymous class implementing an interface.

Intuition. Mechanism. The compiler generates a hidden class implementing the interface and instantiates it. The anonymous class can capture (read) effectively-final local variables from the enclosing scope, which is what makes it a self-contained bundle of behavior.

Concrete bite. The ceremony is the problem: four lines (new Greeter() { @Override public String greet... }) to express one line of actual logic. For an interface with a single method, almost all of that is noise — which is precisely what a lambda removes.

💡 Earned rule. Reach for an anonymous class when you need a one-off implementation that has multiple methods or needs its own fields; otherwise prefer the lambda in the next section. The cost of an anonymous class is verbosity and a distinct generated class; the benefit is a complete inline implementation when a single-expression lambda isn't enough.


3. Lambdas

When the interface has exactly one abstract method — a functional interface — a lambda expresses the implementation as just parameters -> body. It's the anonymous class of §2 with all the ceremony removed, and it makes behavior a value.

Output:

Hello, Ada
running

Analysis. name -> "Hello, " + name is a Greeter — exactly the anonymous class from §2, written as one expression. The compiler infers that name is the parameter of greet and the expression is its return value. Runnable (a built-in functional interface, void run()) works the same way with () for no parameters. The lambda is an object you stored in g and r and called later — behavior as a value.

Intuition. Mechanism. A lambda is an implementation of a functional interface — one abstract method — so the compiler knows exactly which method the lambda body defines. The parameter types and return are inferred from that single method's signature.

Concrete bite. "Exactly one abstract method" is a hard requirement: a lambda can't target an interface with two:

Compiler error:

Main.java:4: error: incompatible types: TwoMethods is not a functional interface
        TwoMethods t = () -> System.out.println("?");
                       ^
    multiple non-overriding abstract methods found in interface TwoMethods

TwoMethods has two abstract methods, so a single lambda body can't say which it implements — "not a functional interface." A lambda needs a one-method target (mark your own with @FunctionalInterface to enforce it); for more than one method, use an anonymous class.

💡 Earned rule. Use a lambda to implement a functional interface compactly — passing behavior to sort, forEach, a callback, a strategy. The cost is that it works only for single-method interfaces (and captures only effectively-final locals); the benefit is enormous expressiveness: logic becomes a value you store and pass, which is the entire premise of the Streams API in Tutorial 28.


4. Method references

A lambda that does nothing but call one existing method can be written even shorter as a method reference: Type::method. It names the method directly instead of wrapping it in x -> x.method().

Output:

[alice, Bob, Charlie]
alice
Bob
Charlie

Analysis. names.sort(String::compareToIgnoreCase) sorted case-insensitively — String::compareToIgnoreCase is a method reference standing in for the lambda (a, b) -> a.compareToIgnoreCase(b), used as a Comparator. names.forEach(System.out::println) printed each name — System.out::println is the lambda s -> System.out.println(s). Both are just shorter spellings of lambdas that delegate to one method.

Intuition. Mechanism. A method reference is sugar for a lambda whose body is a single method call; the compiler matches the referenced method's shape to the functional interface's method. The forms — Type::staticMethod, Type::instanceMethod, object::instanceMethod, Type::new — all expand to the equivalent lambda.

Concrete bite. They read as the intent: sort(String::compareToIgnoreCase) says "sort by case-insensitive comparison" with no boilerplate parameters. The boundary is that a method reference only works when the lambda is exactly one call with matching arguments — any extra logic (a transform, a condition) needs a full lambda.

💡 Earned rule. Prefer a method reference when a lambda's whole body is one method call with matching arguments (String::toUpperCase, System.out::println, Objects::nonNull); use a full lambda when there's any additional logic. The cost is learning the four reference forms; the benefit is code that names the operation directly — maximally concise and readable, especially in the stream pipelines ahead.


5. Mental-model summary

Principle Consequence
A static nested class has no enclosing instance; an inner class does Inner classes read the outer instance's fields (and hold a hidden reference to it)
An anonymous class implements an interface inline, unnamed Good for one-off multi-method implementations; verbose for one method
A lambda implements a functional interface (one abstract method) params -> body is behavior as a value you store, pass, and call
A lambda's target must have exactly one abstract method Two methods → "not a functional interface"; mark yours @FunctionalInterface
A method reference replaces a lambda that just calls one method String::toUpperCase, System.out::println — names the operation directly

6. Gotcha checklist

  • X is not a functional interface the target interface has more than one abstract method; use an anonymous class, or reduce it to one method.
  • A lambda can't change a local variable it uses → captured locals must be effectively final; use a field or an array/holder if you must mutate.
  • A nested class holds its outer object alive unexpectedly → it's a non-static inner class; make it static if it doesn't need the enclosing instance.
  • A method reference won't compile where a lambda would → the method's shape doesn't match the interface, or there's extra logic; write the full lambda.
  • Reached for an anonymous class for a one-method interface → a lambda is shorter and clearer; reserve anonymous classes for multi-method or stateful cases.

🧪 Predict, then check. Write a functional interface IntOp { int apply(int a, int b); } and predict what IntOp add = (a, b) -> a + b; System.out.println(add.apply(3, 4)); prints. Next, rewrite the §2 anonymous Greeter as a lambda and confirm identical output. Finally, predict the output of sorting ["banana","Apple","cherry"] with String::compareToIgnoreCase then printing with forEach(System.out::println), and explain what lambda each method reference stands for.

Your Turn

Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.

Mark as read