Lambda Expressions Summary
We’ve covered the foundations of lambda expressions in Java. Before moving deeper into functional interfaces, let’s connect everything we’ve learned.
1. Lambda Expressions
The best mental model for a lambda is:
A lambda expression is an anonymous piece of behavior that can be passed to another method.
For example:
products.forEach(
product -> System.out.println(product)
);Think of the relationship as:
Lambda Expression
↓
provides behavior
↓
Functional Interface
↓
passed to another methodThe important point is that the lambda itself doesn’t decide which functional interface it implements.
The target context does.
For example:
products.forEach(product -> System.out.println(product));forEach() expects:
Consumer<Product>Therefore, Java interprets the lambda as an implementation of Consumer<Product>.
2. Functional Interfaces
A functional interface has exactly one abstract method.
@FunctionalInterface
interface ProductFilter {
boolean accept(Product product);
}It may still contain:
- default methods
- static methods
- inherited methods
The important rule is:
Exactly 1 abstract method
↓
Functional InterfaceTherefore:
0 abstract methods ❌
1 abstract method ✅
2+ abstract methods ❌Functional interfaces provide the contract, while lambdas provide the implementation.
3. Lambda Syntax
The basic syntax is:
(parameters) -> bodyFor example:
(p1, p2) -> Double.compare(p1.getPrice(), p2.getPrice())Java provides several shortcuts.
No Parameters
() -> System.out.println("Hello")One Parameter
product -> product.getName()Parentheses are optional for one untyped parameter.
Multiple Parameters
(p1, p2) -> p1.getPrice() - p2.getPrice()Single Expression
Instead of:
product -> {
return product.getPrice();
}we can write:
product -> product.getPrice()A useful rule:
One expression
↓
No { }
No return
No ;4. Capturing Variables
Lambdas can access variables from their surrounding scope.
double priceLimit = 10.00;
products.removeIf(
product -> product.getPrice() > priceLimit
);Here, the lambda captures priceLimit.
Captured local variables must be:
final or effectively final
This works:
double priceLimit = 10.00;
product -> product.getPrice() > priceLimitThis doesn’t:
double priceLimit = 10.00;
priceLimit = 20.00; // reassigned
product -> product.getPrice() > priceLimitRemember:
Captured local variable
↓
Must remain unchanged
↓
Effectively final5. Avoid Unnecessary Side Effects
A lambda can modify the state of an object outside itself:
List<Product> result = new ArrayList<>();
products.forEach(product ->
result.add(product)
);This is legal because we’re modifying the object — not reassigning the result variable.
Get Suraj’s stories in your inbox
Join Medium for free to get updates from this writer.
But it introduces a side effect:
Lambda
│
└──── modifies ────► External StateThis can make code harder to understand and becomes especially problematic with parallel execution.
When possible, prefer:
List<Product> result =
products.stream()
.filter(product -> product.getPrice() < 10)
.toList();Think:
Prefer transformations over shared mutable state.
This is a guideline rather than an absolute rule — some operations intentionally have side effects.
6. this and super
Lambda expressions don’t introduce their own object scope.
Therefore:
Inside Lambda
│
├── this ──► enclosing class
│
└── super ──► superclass of enclosing classThis differs from anonymous classes.
The easiest rule to remember:
Inside a lambda,
thisandsupermean exactly what they mean in the surrounding code.
7. Checked Exceptions
A lambda’s ability to throw checked exceptions depends on its target functional interface.
Consider:
products.forEach(product ->
writer.write(product.getName())
);writer.write() throws:
IOExceptionBut forEach() expects a Consumer<T>:
void accept(T value);There is no:
throws IOExceptionTherefore:
Lambda
↓
implements
↓
Consumer.accept()
↓
No throws IOException
↓
❌ Lambda cannot propagate IOExceptionWe must either:
- handle the exception inside the lambda
- wrap it in an unchecked exception
- or use a traditional loop
Sometimes:
for (Product product : products) {
writer.write(product.getName());
}is simply clearer.
Don’t use a lambda just because you can. Choose the clearest solution.
8. Method References
When a lambda does nothing except call an existing method, we can often replace it with a method reference.
Instead of:
product -> System.out.println(product)write:
System.out::printlnThink:
Lambda
↓
Only calls existing method?
↓
YES
↓
Consider Method ReferenceThere are four common forms:
Just like lambdas, method references must match a functional interface.
Putting Everything Together
Here’s the complete mental model:
Functional Interface
│
defines the contract
│
▼
┌──────────────┐
│ Behavior │
└──────────────┘
▲ ▲
│ │
Lambda Method
Expression Reference
│ │
└────┬─────┘
│
▼
Passed to a method
│
▼
Method executes itFor example:
products.forEach(System.out::println);can be understood as:
forEach()
↓
expects Consumer<Product>
↓
Consumer defines accept(Product)
↓
System.out::println matches it
↓
println is executed for each ProductThat connection between lambdas, functional interfaces, and method references is the foundation for understanding functional programming in Java.
Quick Revision
Lambda
→ Anonymous behavior passed aroundFunctional Interface
→ Exactly one abstract methodLambda Syntax
→ parameters -> bodyCaptured Variable
→ Must be effectively finalSide Effects
→ Avoid unnecessary external mutationthis / super
→ Same meaning as enclosing codeChecked Exceptions
→ Controlled by the functional interfaceMethod Reference
→ Reuse an existing method
Final Takeaway
If there’s one idea to remember from this module, it’s this:
Functional interfaces define what behavior should look like. Lambdas and method references provide that behavior, allowing us to pass code around in a concise and expressive way.
With these foundations in place, the next step is to explore Java’s standard functional interfaces and understand when to use interfaces such as Predicate, Function, Consumer, and Supplier.









