Robust Oop
Nested & Anonymous Classes; Lambdas
Classes can nest inside other classes: static nested, or inner, where every inner object gets a link to its enclosing instance when it's created. An anonymous class implements an interface inline. A lambda is a compact implementation of a functional interface (one abstract method), turning behavior into a value you can pass and store. Lambdas capture only effectively final locals, java.util.function supplies Predicate, Function, Supplier and Consumer, and method references, in four forms, shorten lambdas that just call a method. The bridge from objects to functional style, shown with verified output.
Suggest an editNested & Anonymous Classes; Lambdas — Behavior as a Value
Until now, a method's behavior has been fixed right where you wrote it. This lesson turns behavior into 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 that implements an interface right where it's used.
- 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 it in a variable, hand it to a method, and call it later. Method references shorten the common case of a lambda that does nothing but call an existing method. Together, they're the bridge from object-oriented to functional Java, and the foundation for streams.
💡 The core idea.
- These tools turn behavior into a value you can pass, store, and call later.
- Nested and anonymous classes let you 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 only calls a method — the bridge to functional Java.
This lesson builds on methods and interfaces. Every output below was produced by compiling and running the code on Java 25.
You'll be able to: create objects of a static nested class and of an inner class, and predict which outer fields each one reads; write an anonymous class and the equivalent lambda for a one-method interface, and predict when a lambda is rejected; predict whether a lambda that uses a local variable compiles; choose Predicate, Function, Supplier or Consumer for a task; rewrite a one-call lambda as a method reference, and name the lambda that a method reference stands for.
📘 How to read the Intuition boxes. Each one is built in three moves:
- The mechanism — what the compiler and the JVM do.
- A concrete bite — a specific, runnable failure (often a real compiler error), shown so the trap is visible.
- The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.
Table of contents
- Nested classes
- Anonymous classes
- Lambdas
- The built-in functional interfaces
- Method references
- Mental-model summary
- Gotcha checklist
- Check yourself
- Sources
1. Nested classes
You can declare a class inside another class. Java has four kinds of nested class [1]:
- A
staticnested class is scoped to its enclosing class, but has no link to any instance of it. - A non-static inner class (a member class) holds a hidden link to an enclosing instance, so it can read that instance's fields.
- A local class is declared inside a block, such as a method body, just like a local variable [2].
- An anonymous class has no name at all (§2).
Output:
15
15Analysis.
StaticNestedneeds noOuterinstance:new Outer.StaticNested().- It can't see
x, becausexbelongs to anOuterobject, andStaticNesteddoesn't have one. Writingn + xinside it givesnon-static variable x cannot be referenced from a static context. Inneris created from anOuterinstance (outer.new Inner()), and reads that instance'sx. SoaddX(5)is5 + 10 = 15.
The static keyword on a nested class means the same thing it means on a field or method: static means "no enclosing instance," and non-static means "tied to one."
Intuition.
Mechanism. Each inner object is tied to exactly one enclosing object, fixed at the moment the inner object is created [1]. That link is how it reaches x. So two inner objects made from two different outer objects read two different sets of fields:
Output:
1
101
2sa and sa2 share a's count, so the second step on a gives 2. Meanwhile, sb works on b's count, starting from 100. A static nested class carries no such link. It's still a member of Outer, so it can use Outer's private members. private static members need no Outer object at all; private instance members it can reach only through an Outer object it's been given.
Concrete bite. An inner object can't exist without its outer object. new Outer.Inner() in main names no enclosing instance, so javac refuses:
Compiler error:
Main.java:11: error: an enclosing instance that contains Outer.Inner is required
Outer.Inner inner = new Outer.Inner();
^
1 errorWrite outer.new Inner() instead, or make the class static if it never needs x. Here's the practical rule: make a nested class static by default, unless it needs the enclosing instance. A non-static inner object holds a reference to its outer object, which keeps that outer object alive for as long as the inner one lives.
💡 Earned rule. Nest a helper class inside the class it serves, to keep it scoped and private. Make it static unless it must access the enclosing instance's state.
- The cost of a non-static inner class: the hidden outer reference (in memory and in coupling).
- The payoff of nesting: locality. The helper lives exactly where it's used, instead of as a separate top-level class.
2. Anonymous classes
When you want to implement an interface just once, right where you need it, you can write an anonymous class. The expression new Interface() { ... } defines an unnamed implementing class and instantiates it, all in one go.
Output:
Hello, AdaAnalysis. 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. Before lambdas, this was how Java passed behavior around — you wrapped it in an anonymous class that implemented an interface.
Intuition. Mechanism. Behind the scenes, the compiler generates a real class with a made-up name, and instantiates it [3]. Compile the program above in a terminal, and list the class files:
$ javac Main.java
$ ls *.class
Greeter.class
Main$1.class
Main.classMain$1 is the anonymous class. It can read the enclosing method's local variables, as long as they're effectively final (§3 defines the term). That makes it a self-contained bundle of behavior.
Concrete bite. The problem is the ceremony: four lines (new Greeter() { @Override public String greet... }) to express one line of logic. For an interface with a single method, almost all of that is noise — and that noise is exactly 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 from the next section.
- The cost: verbosity, plus a separate generated class.
- The payoff: a complete inline implementation, for when a single-expression lambda isn't enough.
3. Lambdas
When an interface has exactly one abstract method — making it a functional interface [4] — a lambda can express the implementation as parameters -> body. It does the same job as the anonymous class from §2, with all the ceremony stripped away, and it makes behavior a value.
Output:
Hello, Ada
runningAnalysis. name -> "Hello, " + name is a Greeter: the same behavior as the anonymous class from §2, written as a single expression.
- The compiler infers that
nameis the parameter ofgreet, and that the expression is its return value. Runnable(a built-in functional interface withvoid run()) works the same way, using()for no parameters.- The lambda is an object, which you stored in
gandrand called later: behavior as a value.
A lambda's body can also be a block with a return, and its parameters can carry explicit types [5]:
Output:
7
4With two parameters, you need parentheses. An expression body returns its value automatically, while a block body needs an explicit return.
Intuition. Mechanism. A lambda implements a functional interface. Because there's only one abstract method, the compiler knows exactly which method the lambda body defines, and it infers the parameter types and return type from that method's signature.
A lambda isn't an anonymous class, though. No Main$2.class shows up for one. Inside a lambda, this and every other name mean exactly what they mean in the surrounding code [6]. Inside an anonymous class, by contrast, this is the anonymous object.
Concrete bite. "Exactly one abstract method" is a hard requirement. A lambda can't target an interface with two:
Compiler error:
Main.java:5: error: incompatible types: TwoMethods is not a functional interface
TwoMethods t = () -> System.out.println("?");
^
multiple non-overriding abstract methods found in interface TwoMethodsTwoMethods has two abstract methods, so a single lambda body can't say which one it implements. For more than one method, use an anonymous class. And mark your own one-method interfaces with @FunctionalInterface: that way, the mistake gets reported at the interface itself, before anyone writes a lambda [7]:
Compiler error:
Main.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
TwoMethods is not a functional interface
multiple non-overriding abstract methods found in interface TwoMethods
1 errorNon-example: changing a variable the lambda uses. A lambda may use one of its method's local variables only if that variable is effectively final — meaning it's never assigned again after its initializer [6]. A single later assignment breaks the rule, even one that comes after the lambda:
Compiler error:
Main.java:6: error: local variables referenced from a lambda expression must be final or effectively final
Greeter g = name -> greeting + ", " + name;
^
1 errorThe lambda might run much later, long after greeting has changed. The JLS explains the reasoning: capturing a changing local "would likely introduce concurrency problems" [6]. To fix it, delete the reassignment, or copy the value into a new variable that never changes.
💡 Earned rule. Use a lambda to implement a functional interface compactly — for example, when passing behavior to sort, forEach, a callback, or a strategy.
- The cost: it works only for single-method interfaces, and it captures only effectively final locals.
- The payoff: logic becomes a value you can store and pass. That's the whole premise of the Streams API.
4. The built-in functional interfaces
In practice, you rarely need to write Greeter or IntOp yourself. The java.util.function package supplies functional interfaces for all the common shapes [8], and four of them cover most uses:
| Interface | Its one method | Shape |
|---|---|---|
Predicate<T> |
boolean test(T t) |
a yes/no question about a value |
Function<T, R> |
R apply(T t) |
turns a T into an R |
Supplier<T> |
T get() |
produces a value from nothing |
Consumer<T> |
void accept(T t) |
does something with a value, returns nothing |
Output:
false
true
5
hi
DONEAnalysis. Each lambda takes the shape of its interface's one method:
isLonganswerstrueorfalse.lengthmaps aStringto anInteger.greetingtakes no argument at all.shoutreturns nothing; it just prints.
The package also has many variants of these four, such as BiFunction<T, U, R> for two arguments, and IntPredicate for an int without boxing.
💡 Earned rule. Before you declare a functional interface, look for one in java.util.function. Ask yourself two questions: how many arguments go in, and does a value come out?
- The cost: the generic names (
apply,test,get,accept) say less about your domain thangreetdoes. - The payoff: every library method that takes a
Predicateor aFunction, streams included, accepts your lambda directly.
5. Method references
When a lambda does nothing but call one existing method, you can write it even more briefly 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
CharlieAnalysis.
names.sort(String::compareToIgnoreCase)sorted the list case-insensitively. Here,String::compareToIgnoreCasestands for the lambda(a, b) -> a.compareToIgnoreCase(b), used as aComparator.names.forEach(System.out::println)printed each name.System.out::printlnis the lambdas -> System.out.println(s).
Both are just shorter spellings of lambdas that delegate to a single method.
Intuition. Mechanism. A method reference refers to a method without calling it [9]. The compiler matches the referenced method's shape against the functional interface's method. There are four forms:
| Form | Example | The lambda it stands for |
|---|---|---|
Type::staticMethod |
Integer::parseInt |
s -> Integer.parseInt(s) |
object::instanceMethod |
prefix::concat |
s -> prefix.concat(s) |
Type::instanceMethod |
String::length |
s -> s.length() — the first argument is the object |
Type::new |
ArrayList::new |
() -> new ArrayList<>() |
Output:
43
Dr. Ada
5
true
[new]String::equalsIgnoreCase with two arguments shows the third form clearly: same.apply("java", "JAVA") runs "java".equalsIgnoreCase("JAVA").
Concrete bite. Method references read as pure intent: sort(String::compareToIgnoreCase) says "sort by case-insensitive comparison," with no boilerplate parameters. But there's a boundary: a method reference works only 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 as soon as there's any additional logic.
- The cost: learning the four reference forms.
- The payoff: concise, readable code that names the operation directly — especially in the stream pipelines ahead.
6. 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) |
| Each inner object is tied to the outer object it was created from | outer.new Inner(); new Outer.Inner() from main does not compile |
| An anonymous class implements an interface inline, unnamed | A real generated class (Main$1); good for one-off multi-method implementations |
| 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"; @FunctionalInterface reports it at the interface |
| A lambda captures only effectively final locals | Reassign a captured variable anywhere, and the lambda does not compile |
java.util.function supplies the common shapes |
Predicate tests, Function maps, Supplier produces, Consumer accepts |
| A method reference replaces a lambda that only calls one method | Four forms: Type::static, obj::method, Type::method, Type::new |
7. Gotcha checklist
| Symptom | Likely cause | Fix |
|---|---|---|
an enclosing instance that contains Outer.Inner is required |
an inner class created with no outer object | outer.new Inner(), or make the class static |
non-static variable x cannot be referenced from a static context, inside a nested class |
a static nested class reading an instance field |
make it an inner class, or pass it an Outer object |
| 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 |
incompatible types: 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 |
Unexpected @FunctionalInterface annotation |
the annotated interface has two abstract methods | remove one, or remove the annotation |
local variables referenced from a lambda expression must be final or effectively final |
the captured variable is reassigned somewhere | don't reassign it; copy it into a new variable |
this inside a lambda is not the object you expected |
a lambda's this is the enclosing object, unlike an anonymous class's |
use an anonymous class if the code needs its own this |
| 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; reserve anonymous classes for multi-method or stateful cases |
✅ Check yourself
One check per objective. Answer before you open anything.
The 🧪 box below: IntOp, the Greeter lambda, and the case-insensitive sort.
Output:
7
Hello, Ada
Apple
banana
cherryadd.apply(3, 4)is7.- The
Greeterlambda printsHello, Ada, just like the §2 anonymous class. String::compareToIgnoreCasestands for(a, b) -> a.compareToIgnoreCase(b), soApplesorts first despite its capital letter.System.out::printlnstands fors -> System.out.println(s).
📚 Sources
- The Java Language Specification, Java SE 25, §8.1.3 "Inner Classes and Enclosing Instances" (member, local and anonymous inner classes; the immediately enclosing instance is fixed at creation) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.1.3
- The Java Language Specification, Java SE 25, §14.3 "Local Class and Interface Declarations" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.3
- The Java Language Specification, Java SE 25, §15.9.5 "Anonymous Class Declarations" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.9.5
- The Java Language Specification, Java SE 25, §9.8 "Functional Interfaces" (one abstract method, "aside from the methods of Object") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.8
- The Java Language Specification, Java SE 25, §15.27 "Lambda Expressions" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.27
- The Java Language Specification, Java SE 25, §15.27.2 "Lambda Body" (captured locals "must either be final or effectively final";
thisas in the surrounding context) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.27.2 - The Java Language Specification, Java SE 25, §9.6.4.9 "
@FunctionalInterface" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.4.9 java.util.function, Java SE 25 API ("Functional interfaces provide target types for lambda expressions and method references") — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/function/package-summary.html- The Java Language Specification, Java SE 25, §15.13 "Method Reference Expressions" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.13
🧪 Predict, then check.
- Write a functional interface
IntOp { int apply(int a, int b); }, and predict whatIntOp add = (a, b) -> a + b; System.out.println(add.apply(3, 4));prints. - Rewrite the §2 anonymous
Greeteras a lambda, and confirm that the output is identical. - Predict the output of sorting
["banana","Apple","cherry"]withString::compareToIgnoreCase, then printing withforEach(System.out::println). Explain which 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.