Sitemap
Suraj

Suraj

Technical Lead | 14+ yrs specializing in Java, Distributed Systems & System Design.

Lambda Expressions Summary

4 min readAug 23, 2026

--

We’ve covered the foundations of lambda expressions in Java. Before moving deeper into functional interfaces, let’s connect everything we’ve learned.

Press enter or click to view image in full size

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 method

The 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 Interface

Therefore:

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) -> body

For 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() > priceLimit

This doesn’t:

double priceLimit = 10.00;

priceLimit = 20.00; // reassigned
product -> product.getPrice() > priceLimit

Remember:

Captured local variable
↓
Must remain unchanged
↓
Effectively final

5. 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 State

This 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 class

This differs from anonymous classes.

The easiest rule to remember:

Inside a lambda, this and super mean 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:

IOException

But forEach() expects a Consumer<T>:

void accept(T value);

There is no:

throws IOException

Therefore:

Lambda
↓
implements
↓
Consumer.accept()
↓
No throws IOException
↓
❌ Lambda cannot propagate IOException

We 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::println

Think:

Lambda
↓
Only calls existing method?
↓
YES
↓
Consider Method Reference

There are four common forms:

Press enter or click to view image in full size

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 it

For 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 Product

That connection between lambdas, functional interfaces, and method references is the foundation for understanding functional programming in Java.

Quick Revision

Lambda
→ Anonymous behavior passed around
Functional Interface
→ Exactly one abstract method
Lambda Syntax
→ parameters -> body
Captured Variable
→ Must be effectively final
Side Effects
→ Avoid unnecessary external mutation
this / super
→ Same meaning as enclosing code
Checked Exceptions
→ Controlled by the functional interface
Method 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.

--

--