Lambda Expressions
Lambda expressions let you write short, anonymous functions right where they’re needed — no boilerplate class, no verbose new Runnable() { ... }. Introduced in Java 8, they’re the foundation of functional-style programming in Java and make code like stream pipelines and event handlers dramatically cleaner.
Why Lambdas?
Before Java 8, passing behavior meant creating an anonymous inner class every time:
// Pre-Java 8 — verbose anonymous class
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello from a thread!");
}
};
new Thread(r).start();
With a lambda, that collapses to one line:
// Java 8+ lambda
Runnable r = () -> System.out.println("Hello from a thread!");
new Thread(r).start();
Same behavior, far less noise.
Syntax
(parameters) -> expression
(parameters) -> { statements; }
| Form | Example |
|---|---|
| No parameters | () -> System.out.println("Hi") |
| One parameter (parens optional) | n -> n * 2 |
| Multiple parameters | (a, b) -> a + b |
| Block body (multi-statement) | (x, y) -> { int sum = x + y; return sum; } |
Note: When the body is a single expression,
returnand braces are omitted. The expression’s value is returned automatically.
Functional Interfaces
A lambda can only be used where a functional interface is expected — an interface with exactly one abstract method. The lambda provides the implementation of that method.
@FunctionalInterface
interface Greeter {
String greet(String name);
}
public class LambdaDemo {
public static void main(String[] args) {
Greeter formal = name -> "Good day, " + name + ".";
Greeter casual = name -> "Hey, " + name + "!";
System.out.println(formal.greet("Alice"));
System.out.println(casual.greet("Bob"));
}
}
Output:
Good day, Alice.
Hey, Bob!
The @FunctionalInterface annotation is optional but recommended — it makes the compiler enforce the “exactly one abstract method” rule. See Functional Interfaces for the full set of built-in ones.
Built-in Functional Interfaces (java.util.function)
Java 8 ships a rich set of ready-to-use functional interfaces so you rarely need to define your own:
| Interface | Method | Use case |
|---|---|---|
Predicate<T> | boolean test(T t) | Filter / test a condition |
Function<T, R> | R apply(T t) | Transform a value |
Consumer<T> | void accept(T t) | Consume a value, no return |
Supplier<T> | T get() | Produce a value, no input |
BiFunction<T, U, R> | R apply(T t, U u) | Two-input transform |
UnaryOperator<T> | T apply(T t) | Transform, same type |
BinaryOperator<T> | T apply(T t1, T t2) | Combine two values, same type |
import java.util.function.*;
public class BuiltInFunctions {
public static void main(String[] args) {
Predicate<Integer> isEven = n -> n % 2 == 0;
Function<String, Integer> length = String::length; // method ref
Consumer<String> printer = System.out::println;
Supplier<String> greeting = () -> "Hello, World!";
System.out.println(isEven.test(4)); // true
System.out.println(length.apply("Java")); // 4
printer.accept("Consuming!"); // Consuming!
System.out.println(greeting.get()); // Hello, World!
}
}
Output:
true
4
Consuming!
Hello, World!
Tip:
String::lengthis a method reference — a shorthand lambda for calling an existing method. See Method References for the full syntax.
Lambdas with Collections
Lambdas shine when sorting and iterating collections. Before Java 8 you’d write a Comparator anonymous class; now it’s a one-liner:
import java.util.*;
public class SortDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>(Arrays.asList("Charlie", "Alice", "Bob"));
// Sort alphabetically
names.sort((a, b) -> a.compareTo(b));
System.out.println(names);
// Sort by length
names.sort(Comparator.comparingInt(String::length));
System.out.println(names);
// Iterate with forEach
names.forEach(name -> System.out.println("Hello, " + name));
}
}
Output:
[Alice, Bob, Charlie]
[Bob, Alice, Charlie]
Hello, Bob
Hello, Alice
Hello, Charlie
Lambdas with the Stream API
The real power of lambdas emerges when combined with the Stream API. Streams let you express data-processing pipelines declaratively:
import java.util.*;
import java.util.stream.*;
public class StreamDemo {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // keep even numbers
.map(n -> n * n) // square them
.collect(Collectors.toList()); // gather results
System.out.println(result);
}
}
Output:
[4, 16, 36, 64, 100]
Each step (filter, map) takes a lambda. The code reads almost like English: “filter the evens, square each one, collect the list.”
Variable Capture
Lambdas can read variables from the enclosing scope, but those variables must be effectively final (never reassigned after the lambda is created):
public class CaptureDemo {
public static void main(String[] args) {
String prefix = "Hello"; // effectively final — never reassigned
Runnable r = () -> System.out.println(prefix + ", Lambda!");
r.run();
// prefix = "Hi"; // Uncommenting this would cause a compile error
}
}
Output:
Hello, Lambda!
Warning: A lambda cannot capture a local variable that is reassigned after the capture point. This rule exists because lambdas may run on a different thread or at a later time — a mutable local would create a data race.
Instance fields and static fields, however, have no such restriction because they live on the heap and are shared by reference.
Lambdas as Parameters
You can pass a lambda wherever a functional interface type is expected, making APIs far more flexible:
import java.util.function.Predicate;
import java.util.*;
import java.util.stream.*;
public class FilterUtil {
static List<String> filter(List<String> list, Predicate<String> condition) {
return list.stream()
.filter(condition)
.collect(Collectors.toList());
}
public static void main(String[] args) {
List<String> words = List.of("apple", "banana", "avocado", "cherry", "apricot");
List<String> aWords = filter(words, w -> w.startsWith("a"));
System.out.println(aWords);
List<String> longWords = filter(words, w -> w.length() > 6);
System.out.println(longWords);
}
}
Output:
[apple, avocado, apricot]
[banana, avocado, apricot]
Under the Hood
When the compiler sees a lambda, it doesn’t create a new .class file the way an anonymous inner class does. Instead, it uses an invokedynamic bytecode instruction (introduced in Java 7, repurposed for lambdas in Java 8). Here’s what happens:
- First call: The JVM calls a bootstrap method in
LambdaMetafactory. This generates a small implementation class at runtime and returns aCallSitepointing to it. - Subsequent calls: The
CallSiteis cached. The generated class is reused — no repeated class generation. - Memory: Because no anonymous
.classfile is created at compile time, the classloader isn’t loaded with extra classes. The runtime-generated class is typically a direct method handle wrapper.
This means lambdas are faster to load than equivalent anonymous inner classes and produce less bytecode bloat in your JAR.
Note: You can inspect the bytecode of a class with the
javaptool (javap -c -p ClassName) and see theinvokedynamicinstruction where lambdas appear.
this Inside a Lambda
Unlike an anonymous inner class, a lambda does not introduce a new scope for this. Inside a lambda, this refers to the enclosing class instance — exactly as it would outside the lambda. This makes lambdas better than anonymous classes when you need to reference the outer object.
public class ThisDemo {
private String name = "DevCraftly";
public void showName() {
Runnable r = () -> System.out.println(this.name); // 'this' = ThisDemo instance
r.run();
}
public static void main(String[] args) {
new ThisDemo().showName();
}
}
Output:
DevCraftly
Quick Reference
| Feature | Lambda | Anonymous Inner Class |
|---|---|---|
| Syntax verbosity | Minimal | High |
this reference | Enclosing class | The anonymous class itself |
| Compiled output | invokedynamic | Separate .class file |
| Can have state/fields | No | Yes |
| Must be functional interface | Yes | No |
Related Topics
- Functional Interfaces — the contract every lambda fulfills, plus all built-in
java.util.functiontypes - Method References — shorthand lambda syntax for calling existing methods
- Stream API — where lambdas deliver their greatest impact, enabling declarative data pipelines
- Default Methods — how Java 8 added behavior to interfaces without breaking existing code
- Comparator — a classic functional interface made elegant with lambdas
- Java 8 Features — overview of every major feature that shipped alongside lambdas