A lambda expression is an anonymous function that implements the single abstract method of a functional interface.
For nearly 20 years, Java had a reputation for being a verbose language. You had to write a lot of code to perform simple tasks.
If you wanted to pass a function to another method (e.g., “do this when the user clicks”), you had to create an entire class to wrap that function.
In 2014, Java 8 arrived, introducing lambda expressions and expanding the language’s functional-style tools.
The Problem: Anonymous Classes
Imagine we want to create a Thread that prints something. Thread needs a Runnable object.
The old way (Java < 8):
Since Runnable is an interface, we cannot instantiate it directly. We have to create an “Anonymous Class” (a class without a name defined on the fly).
// LOTS OF NOISE
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("I'm an old-school thread");
}
});
thread.start();
Look closely. Of those 6 lines, only 1 matters (System.out...).
Everything else (new Runnable, public void run, braces, parentheses) is boilerplate code that only serves to satisfy the compiler.
The Solution: Lambda Expressions (->)
A Lambda Expression is, simplifying greatly, a way to treat code as if it were data. Instead of passing an object that has a method, we pass the behavior directly.
The modern way (Java 8+):
// ZERO NOISE
Thread thread = new Thread(() -> System.out.println("I'm a modern thread"));
thread.start();
See the difference? We’ve gone from 5 lines of “ceremony” to 1 line of pure logic.
The Arrow Syntax
A Lambda has three parts:
(Parameters) -> { Body }
- Parameters: What the function receives (on the left).
- Arrow Operator (
->): Separates the parameters from the action. - Body: What the function does (on the right).
Simplifying a Lambda Expression
The best part about Lambdas is that Java is very clever at deducing types (Type Inference). Let’s see how the code shrinks step by step using a Comparator of Strings.
Step 1: Full Lambda (Explicit)
// We specify types and use braces and return
(String s1, String s2) -> {
return s1.length() - s2.length();
}
Step 2: Remove Types (Inference) Java knows we are comparing Strings, no need to state it.
(s1, s2) -> {
return s1.length() - s2.length();
}
Step 3: Remove Braces and Return (Simple Expression)
If the body is a single line, we can delete the braces {} and the return keyword. It is implicitly assumed.
(s1, s2) -> s1.length() - s2.length()
Boom! From a giant anonymous class, we’ve gone to a single, super readable line.
Rule for a single parameter:
If the lambda receives ONLY ONE parameter, you can also remove the parentheses on the left.
name -> System.out.println("Hello " + name)
Where Can I Use Lambdas?
Here’s the catch. You can’t use a Lambda wherever you feel like it. You can only use a Lambda where Java expects a Functional Interface.
And what is a Functional Interface? It is an interface that has EXACTLY ONE abstract method.
Runnable: Only hasrun(). -> Yes it works.Comparator: Only hascompare(). -> Yes it works.ActionListener: Only hasactionPerformed(). -> Yes it works.
If the interface had 2 abstract methods, Java wouldn’t know which one your Lambda corresponds to, and it would give an error.
Many older interfaces (Runnable, Callable) became functional retroactively. Additionally, Java created an entire package java.util.function full of new interfaces ready to use, which we will see in the next article.
External Variables
Lambdas can use variables that are outside their body (this is called a Closure), but with a strict condition: The variable cannot change.
int counter = 10;
Runnable r = () -> {
// We can READ counter
System.out.println("Value: " + counter);
// counter++; // COMPILATION ERROR!
};
Java requires the variable to be final or “effectively final” (never modified after initialization). If you need to modify external variables, you’ll have to use atomic wrappers or single-element arrays (hacks we’ll explore later).