Lambda Expressions
Lambda expressions (Java 8) give Java a concise way to represent behavior as data. A lambda is an anonymous function: a parameter list, an arrow, and a body. Combined with functional interfaces, lambdas let you pass logic to methods — the foundation of the Streams API and modern, declarative Java.
Syntax
A lambda has three parts: parameters, the -> arrow, and a body.
// Full form
(int a, int b) -> { return a + b; }
// Inferred types, single expression (implicit return)
(a, b) -> a + b
// Single parameter — parentheses optional
x -> x * 2
// No parameters
() -> System.currentTimeMillis()
The compiler infers parameter types from the target type — the functional interface the lambda is assigned to.
Functional Interfaces
A functional interface has exactly one abstract method (the SAM — Single Abstract Method). A lambda is an implementation of that method. java.util.function ships the core interfaces.
| Interface | Abstract method | Meaning |
|---|---|---|
Function<T,R> | R apply(T t) | Transform a value |
Predicate<T> | boolean test(T t) | Test a condition |
Consumer<T> | void accept(T t) | Consume, no return |
Supplier<T> | T get() | Produce a value |
BiFunction<T,U,R> | R apply(T t, U u) | Two-arg transform |
UnaryOperator<T> | T apply(T t) | T → T |
import java.util.function.*;
Function<String, Integer> length = s -> s.length();
Predicate<Integer> isEven = n -> n % 2 == 0;
Consumer<String> printer = System.out::println;
Supplier<Double> random = Math::random;
System.out.println(length.apply("hello"));
System.out.println(isEven.test(4));
printer.accept("done");
Output:
5
true
done
Note: Annotate your own functional interfaces with
@FunctionalInterface. The compiler then enforces the single-abstract-method rule and rejects accidental additions.
Method References
When a lambda merely calls an existing method, a method reference (::) is clearer. There are four kinds.
| Kind | Syntax | Example |
|---|---|---|
| Static method | Class::staticMethod | Integer::parseInt |
| Instance of a particular object | instance::method | System.out::println |
| Instance of an arbitrary object | Class::instanceMethod | String::toUpperCase |
| Constructor | Class::new | ArrayList::new |
import java.util.*;
import java.util.stream.Collectors;
List<String> names = List.of("ada", "grace", "linus");
List<String> upper = names.stream()
.map(String::toUpperCase) // arbitrary-object instance ref
.collect(Collectors.toList());
Supplier<List<String>> factory = ArrayList::new; // constructor ref
System.out.println(upper);
Output:
[ADA, GRACE, LINUS]
Effectively-Final Capture
A lambda can read local variables from its enclosing scope, but only if they are effectively final — assigned once and never reassigned.
int factor = 3; // effectively final
Function<Integer, Integer> scale = n -> n * factor;
System.out.println(scale.apply(10)); // 30
// factor = 4; // would break the lambda: compile error
Output:
30
Warning: Reassigning a captured variable causes a compile error. This restriction prevents subtle bugs where a lambda would observe a value that changed after capture. Instance and static fields are not restricted because they live on the heap.
Lambdas vs Anonymous Classes
Lambdas often replace anonymous classes, but they differ in important ways.
// Anonymous class
Runnable a = new Runnable() {
@Override public void run() {
System.out.println(this.getClass()); // refers to the anon class
}
};
// Lambda
Runnable b = () -> System.out.println("lambda");
| Aspect | Anonymous class | Lambda |
|---|---|---|
this | The anonymous instance | The enclosing instance |
New .class file | Yes | No (invokedynamic) |
| Can have state/fields | Yes | No |
| Implements | Any interface/class | Functional interface only |
| Shadowing variables | Allowed | Not allowed |
Lambdas are lighter at runtime (no extra class loaded) and clearer for single-method behavior. Use an anonymous class when you need fields, multiple methods, or to extend a class.
Best Practices
- Keep lambda bodies short; extract complex logic into named methods and reference them.
- Prefer method references over lambdas when they read more clearly.
- Name your custom functional interfaces by intent and annotate with
@FunctionalInterface. - Keep lambdas pure — avoid side effects, especially inside stream pipelines.
- Do not capture mutable state; rely on effectively-final locals or pass values explicitly.
- Favor standard
java.util.functioninterfaces over reinventing your own.