Java Lambda Expressions
A lambda expression is a concise way to pass behaviour as data. Lambdas implement functional interfaces (interfaces with a single abstract method) in a single line. They are the foundation of the modern Java functional style, used heavily with the Streams API.
This tutorial covers lambda syntax, functional interfaces, method references, capturing variables, and the standard functional interfaces in java.util.function.
1. Lambda Syntax
A lambda has three parts: a parameter list, an arrow ->, and a body. The body can be a single expression or a block:
class=class="tok-str">"tok-cmt">// single expression body
Runnable r = () -> System.out.println("hi");
Comparator<Integer> cmp = (a, b) -> a - b;
class=class="tok-str">"tok-cmt">// block body with return
Function<Integer, Integer> square = (Integer n) -> {
int result = n * n;
return result;
};
class=class="tok-str">"tok-cmt">// no parameters
Runnable noop = () -> {};
class=class="tok-str">"tok-cmt">// one parameter without parentheses
Function<String, Integer> length = s -> s.length();
class=class="tok-str">"tok-cmt">// multiple parameters
BinaryOperator<Integer> sum = (a, b) -> a + b;
class=class="tok-str">"tok-cmt">// with type inference explicit
BinaryOperator<Integer> sum2 = (Integer a, Integer b) -> a + b;
The compiler infers the parameter types from the target functional interface. You can omit them entirely. For a single-parameter lambda, the parentheses around the parameter are also optional.
2. Functional Interfaces
A functional interface has exactly one abstract method. The @FunctionalInterface annotation makes the compiler check this rule:
@FunctionalInterface
public interface StringFunction {
String apply(String input);
}
class=class="tok-str">"tok-cmt">// usage
StringFunction upper = s -> s.toUpperCase();
StringFunction exclaim = s -> s + "!";
String result = upper.apply("hello"); class=class="tok-str">"tok-cmt">// "HELLO"
class=class="tok-str">"tok-cmt">// built-in functional interface
java.util.function.Function<String, Integer> len = s -> s.length();
java.util.function.Predicate<String> isEmpty = s -> s.isEmpty();
java.util.function.Consumer<String> print = s -> System.out.println(s);
java.util.function.Supplier<List<String>> empty = () -> new ArrayList<>();
Lambdas are not objects in the JVM sense — the compiler generates an implementation of the target interface at compile time, and the JVM may reuse a single instance if the lambda does not capture state.
3. Method References
When a lambda just calls a single method, you can replace it with a method reference — ClassName::methodName. Four kinds:
class=class="tok-str">"tok-cmt">// class="tok-num">1. static method reference
Function<String, Integer> parser = Integer::parseInt;
class=class="tok-str">"tok-cmt">// equivalent to: s -> Integer.parseInt(s)
class=class="tok-str">"tok-cmt">// class="tok-num">2. instance method reference (on a specific object)
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;
class=class="tok-str">"tok-cmt">// equivalent to: s -> prefix.concat(s)
class=class="tok-str">"tok-cmt">// class="tok-num">3. instance method reference (unbound, first param becomes the receiver)
Function<String, Integer> length = String::length;
class=class="tok-str">"tok-cmt">// equivalent to: s -> s.length()
class=class="tok-str">"tok-cmt">// class="tok-num">4. constructor reference
Supplier<ArrayList<String>> factory = ArrayList::new;
class=class="tok-str">"tok-cmt">// equivalent to: () -> new ArrayList<>()
Function<Integer, String[]> newArray = String[]::new;
class=class="tok-str">"tok-cmt">// equivalent to: n -> new String[n]
Method references are nearly always more readable than the equivalent lambda. Reach for them when the lambda body is a single method call.
4. Capturing Variables
A lambda can use variables from its enclosing scope. The captured variables must be effectively final — assigned once and never changed:
class=class="tok-str">"tok-cmt">// captured local - must be effectively final
String greeting = "Hello, ";
Runnable r = () -> System.out.println(greeting + "world");
class=class="tok-str">"tok-cmt">// greeting = "Hi, "; // would be a compile error - lambda captures greeting
class=class="tok-str">"tok-cmt">// captured effectively-final parameter
public void process(List<String> names, String suffix) {
names.forEach(n -> System.out.println(n + suffix));
class=class="tok-str">"tok-cmt">// suffix is effectively final - never reassigned in this method
}
class=class="tok-str">"tok-cmt">// mutable workaround - capture an array reference
int[] counter = {class="tok-num">0};
Runnable inc = () -> counter[class="tok-num">0]++;
inc.run();
inc.run();
System.out.println(counter[class="tok-num">0]); class=class="tok-str">"tok-cmt">// class="tok-num">2
This restriction enables safe concurrent execution: the captured values are immutable snapshots, so a lambda running on another thread cannot see a torn read.
5. Standard Functional Interfaces
The java.util.function package has dozens of functional interfaces. The most-used:
| Interface | Method | Use |
|---|---|---|
Predicate<T> | boolean test(T) | Filter / boolean test |
Consumer<T> | void accept(T) | Side effect on input |
Supplier<T> | T get() | Produce a value |
Function<T,R> | R apply(T) | Transform input to output |
BiFunction<T,U,R> | R apply(T,U) | Two inputs, one output |
UnaryOperator<T> | T apply(T) | Input and output same type |
BinaryOperator<T> | T apply(T,T) | Combine two values |
import java.util.function.*;
class=class="tok-str">"tok-cmt">// Predicate - boolean test
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> notEmpty = isEmpty.negate();
boolean empty = isEmpty.test("");
class=class="tok-str">"tok-cmt">// Consumer - side effect
Consumer<String> printer = System.out::println;
Consumer<String> debug = printer.andThen(s -> System.err.println("DEBUG: " + s));
class=class="tok-str">"tok-cmt">// Supplier - produce a value
Supplier<Double> random = Math::random;
double v = random.get();
class=class="tok-str">"tok-cmt">// Function - transform
Function<String, Integer> length = String::length;
Function<Integer, String> toStr = Object::toString;
class=class="tok-str">"tok-cmt">// BiFunction - two args
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
class=class="tok-str">"tok-cmt">// UnaryOperator - same input/output type
UnaryOperator<String> trim = String::trim;
class=class="tok-str">"tok-cmt">// BinaryOperator - combine two of same type
BinaryOperator<Integer> max = Integer::max;
6. Composing Functions
Function and Predicate compose with default methods like andThen and compose:
import java.util.function.Function;
Function<Integer, Integer> plusOne = x -> x + class="tok-num">1;
Function<Integer, Integer> timesTwo = x -> x * class="tok-num">2;
class=class="tok-str">"tok-cmt">// andThen - apply this, then the other
Function<Integer, Integer> combined = plusOne.andThen(timesTwo);
class=class="tok-str">"tok-cmt">// combined.apply(class="tok-num">3) = (class="tok-num">3 + class="tok-num">1) * class="tok-num">2 = class="tok-num">8
class=class="tok-str">"tok-cmt">// compose - apply the other first, then this
Function<Integer, Integer> composed = plusOne.compose(timesTwo);
class=class="tok-str">"tok-cmt">// composed.apply(class="tok-num">3) = (class="tok-num">3 * class="tok-num">2) + class="tok-num">1 = class="tok-num">7
class=class="tok-str">"tok-cmt">// predicate composition
Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> longerThan3 = s -> s.length() > class="tok-num">3;
Predicate<String> valid = notEmpty.and(longerThan3);
boolean ok = valid.test("hello"); class=class="tok-str">"tok-cmt">// true
7. Pitfalls
int[] counter = {0}; nums.forEach(n -> counter[0]++); compiles — the array reference is effectively final, but its contents are mutated. This works but is fragile; use a stream reduction (count()) or an AtomicInteger instead.
A lambda cannot throw a checked exception unless the functional interface's method declares it. If you need to call a checked-exception-throwing method from a lambda, either catch inside the lambda or wrap the exception in a runtime exception.
Exercises
- Rewrite an anonymous
Comparator<String>as a lambda, then as a method reference. - Use
Predicate<String>to filter a list of strings to those longer than 4 characters. - Use
Function<String, Integer>to map strings to their lengths, then compose with another function that doubles the length. - Use
Supplier<List>withStream.generateto create an infinite stream, then limit it.