All Styles of Lambda Expressions in Java
A practical guide to Java lambda expression syntax, parameter styles, expression and block bodies, method references, functional interfaces, and rules from Java 8 through modern Java.
All Styles of Lambda Expressions in Java
Lambda expressions were introduced in Java 8 to represent a small piece of behavior that can be passed around as a value. A lambda is not a standalone function: Java gives it a type through a functional interface, which has exactly one abstract method.
The general form is:
(parameters) -> expression
(parameters) -> { statements }
This article covers the major lambda styles available in modern Java, including syntax variants, typing, annotations, method references, and common usage patterns.
Basic Lambda Syntax
| Style | Example | Notes |
|---|---|---|
| No parameters | () -> "Hello" | Empty parentheses are required. |
| One untyped parameter | name -> name.length() | Parentheses may be omitted for exactly one untyped parameter. |
| One parenthesized parameter | (name) -> name.length() | Equivalent to the previous form and often clearer in longer expressions. |
| Multiple parameters | (a, b) -> a + b | Parameters must be separated by commas. |
| Typed parameters | (int a, int b) -> a + b | All parameters are explicitly typed. |
| Inferred parameters | (a, b) -> a + b | Types are inferred from the target functional interface. |
| Expression body | x -> x * x | The expression's value is returned automatically. |
| Block body | x -> { return x * x; } | Uses braces and requires an explicit return for a value. |
Functional Interface Target Types
A lambda needs a target type. The target is usually a variable declaration, method argument, return type, or cast.
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
Function<String, Integer> length = text -> text.length();
Predicate<Integer> isEven = number -> number % 2 == 0;
Supplier<String> greeting = () -> "Hello, Java";
The same lambda syntax can represent different behavior depending on the target type:
Function<Integer, Integer> doubleValue = number -> number * 2;
Predicate<Integer> isPositive = number -> number > 0;
Consumer<Integer> printValue = number -> System.out.println(number);
A lambda cannot be assigned to Object, var, or a concrete class without a functional-interface target:
// Object value = number -> number + 1; // Does not compile
// var value = number -> number + 1; // Does not compile
Function<Integer, Integer> value = number -> number + 1;
Lambda Body Styles
Expression Body
An expression body contains one expression. Its result is returned automatically when the functional interface has a return value.
Function<String, String> upper = text -> text.toUpperCase();
BinaryOperator<Integer> add = (left, right) -> left + right;
Predicate<String> nonEmpty = text -> !text.isEmpty();
For a void-returning functional interface, the expression can be a statement expression such as a method call or assignment:
Consumer<String> logger = message -> System.out.println(message);
Consumer<StringBuilder> append = builder -> builder.append("Java");
Block Body Without a Return Value
Use a block body when the lambda performs one or more statements and returns void.
Consumer<String> printDetails = text -> {
System.out.println("Text: " + text);
System.out.println("Length: " + text.length());
};
Block Body With a Return Value
A value-returning block lambda must explicitly return a value on every possible path.
Function<Integer, String> classify = number -> {
if (number > 0) {
return "positive";
}
if (number < 0) {
return "negative";
}
return "zero";
};
A block lambda cannot mix an expression-style result with return:
// Invalid: either use an expression body or a block with return.
// Function<Integer, Integer> square = number -> { number * number; };
Nested Lambda Bodies
A lambda can create or return another lambda. This is useful for factories and function composition.
Function<Integer, Function<Integer, Integer>> addFactory = first -> second -> first + second;
Function<Integer, Integer> addTen = addFactory.apply(10);
int result = addTen.apply(5); // 15
Parameter Styles
No Parameters
Use () even when the lambda does not receive an argument.
Runnable task = () -> System.out.println("Running");
Supplier<Double> randomValue = () -> Math.random();
One Inferred Parameter
Parentheses are optional for one inferred parameter.
Predicate<String> longText = text -> text.length() > 100;
The parenthesized form is also valid:
Predicate<String> longText = (text) -> text.length() > 100;
You cannot write a typed single parameter without parentheses:
// Invalid: a typed lambda parameter requires parentheses.
// Function<String, Integer> length = String text -> text.length();
Function<String, Integer> length = (String text) -> text.length();
Multiple Inferred Parameters
Comparator<String> byLength = (first, second) ->
Integer.compare(first.length(), second.length());
Parentheses are mandatory when there are two or more parameters.
Explicitly Typed Parameters
Explicit types can improve readability or resolve an overloaded call.
BinaryOperator<Long> maximum = (Long first, Long second) ->
Math.max(first, second);
When one parameter is explicitly typed, every parameter must be explicitly typed:
// Invalid: parameter typing cannot be mixed.
// BiFunction<Integer, Integer, Integer> sum = (int first, second) -> first + second;
BiFunction<Integer, Integer, Integer> sum =
(Integer first, Integer second) -> first + second;
var Parameters Since Java 11
Java 11 allows var in lambda parameters. Use var for every parameter in that lambda, not only some of them. This is especially useful when adding annotations.
BiFunction<String, String, String> join =
(var first, var second) -> first.trim() + " " + second.trim();
Annotated parameters can use var:
BiFunction<String, String, String> normalize =
(@Deprecated var first, var second) -> first.trim() + second.trim();
These forms are invalid:
// Invalid: cannot mix var and inferred parameters.
// (var first, second) -> first + second
// Invalid: cannot mix var and explicit types.
// (var first, String second) -> first + second
Annotated Parameters
Annotations can be placed on explicitly typed parameters or var parameters.
BiFunction<String, String, String> combine =
(@Nonnull String first, @Nonnull String second) -> first + second;
The annotation must be applicable to lambda parameters and must be available on the classpath when compiling.
Standard Functional Interfaces
The java.util.function package provides common target types for lambdas.
| Interface | Method | Input | Output | Example |
|---|---|---|---|---|
Runnable | run() | None | None | () -> log("done") |
Supplier<T> | get() | None | T | () -> createUser() |
Consumer<T> | accept(T) | T | None | user -> save(user) |
BiConsumer<T, U> | accept(T, U) | T, U | None | (key, value) -> cache.put(key, value) |
Function<T, R> | apply(T) | T | R | text -> text.length() |
BiFunction<T, U, R> | apply(T, U) | T, U | R | (a, b) -> a + b |
UnaryOperator<T> | apply(T) | T | T | number -> number * 2 |
BinaryOperator<T> | apply(T, T) | T, T | T | (a, b) -> a.max(b) |
Predicate<T> | test(T) | T | boolean | text -> text.isBlank() |
BiPredicate<T, U> | test(T, U) | T, U | boolean | (a, b) -> a.equals(b) |
IntFunction<R> | apply(int) | int | R | number -> String.valueOf(number) |
ToIntFunction<T> | applyAsInt(T) | T | int | text -> text.length() |
IntSupplier | getAsInt() | None | int | () -> 42 |
IntConsumer | accept(int) | int | None | number -> print(number) |
IntPredicate | test(int) | int | boolean | number -> number > 0 |
Primitive-specialized interfaces such as IntFunction, LongConsumer, and DoublePredicate can avoid boxing and unboxing.
Custom Functional Interfaces
Use @FunctionalInterface to document and validate an interface intended for lambda use.
@FunctionalInterface
interface Formatter {
String format(String input);
}
Formatter markdown = text -> "**" + text + "**";
System.out.println(markdown.format("Java"));
A functional interface may inherit methods from Object and may contain default or static methods. It must still have exactly one abstract method.
@FunctionalInterface
interface Validator<T> {
boolean isValid(T value);
default boolean isInvalid(T value) {
return !isValid(value);
}
static <T> Validator<T> alwaysValid() {
return value -> true;
}
}
Lambdas With Collections and Streams
forEach
List<String> names = List.of("Ada", "Grace", "James");
names.forEach(name -> System.out.println(name));
Filtering
List<String> longNames = names.stream()
.filter(name -> name.length() > 4)
.toList();
Mapping
List<Integer> lengths = names.stream()
.map(name -> name.length())
.toList();
Sorting
List<String> sorted = names.stream()
.sorted((first, second) -> first.compareToIgnoreCase(second))
.toList();
Reducing
int totalLength = names.stream()
.mapToInt(name -> name.length())
.sum();
Collecting Into a Map
Map<String, Integer> nameLengths = names.stream()
.collect(Collectors.toMap(name -> name, name -> name.length()));
For this common identity-key case, a method reference is shorter:
Map<String, Integer> nameLengths = names.stream()
.collect(Collectors.toMap(Function.identity(), String::length));
Method References: The Related Shorter Style
A method reference is not technically a lambda expression, but it is an interchangeable way to express many lambda operations. It uses :: and refers to an existing method.
Static Method Reference
Function<String, Integer> parse = Integer::parseInt;
Equivalent lambda:
Function<String, Integer> parse = text -> Integer.parseInt(text);
Bound Instance Method Reference
String prefix = "java";
Predicate<String> startsWithPrefix = prefix::startsWith;
Equivalent lambda:
Predicate<String> startsWithPrefix = text -> prefix.startsWith(text);
Unbound Instance Method Reference
Function<String, Integer> length = String::length;
Equivalent lambda:
Function<String, Integer> length = text -> text.length();
Constructor Reference
Supplier<ArrayList<String>> createList = ArrayList::new;
Function<Integer, ArrayList<String>> createSizedList = ArrayList::new;
The target interface determines which constructor overload is selected.
Array Constructor Reference
IntFunction<String[]> createArray = String[]::new;
String[] values = createArray.apply(3);
Capturing Variables
A lambda can read local variables, method parameters, fields, and this from its surrounding scope.
Capturing an Effectively Final Local Variable
A local variable is effectively final when it is assigned once and never changed afterward.
String prefix = "ID-";
Function<Integer, String> createId = number -> prefix + number;
The variable does not need the final keyword, but it cannot be reassigned after being captured:
String prefix = "ID-";
Function<Integer, String> createId = number -> prefix + number;
// prefix = "USER-"; // Invalid because prefix is captured.
Capturing an Instance Field
Instance fields may be read or changed, because they belong to the object rather than being local variables.
class Counter {
private int count;
Runnable increment = () -> count++;
}
Capturing this
Inside a lambda, this refers to the enclosing object. This differs from an anonymous inner class, where this refers to the anonymous class instance.
class Printer {
private final String prefix = "LOG: ";
Consumer<String> printer() {
return message -> System.out.println(this.prefix + message);
}
}
Returning and Passing Lambdas
Returning a Lambda
static Predicate<String> longerThan(int minimumLength) {
return text -> text.length() > minimumLength;
}
Passing a Lambda as an Argument
static List<String> select(List<String> values, Predicate<String> condition) {
return values.stream()
.filter(condition)
.toList();
}
List<String> result = select(names, name -> name.startsWith("A"));
Storing Lambdas in a Collection
List<Predicate<String>> checks = List.of(
text -> !text.isBlank(),
text -> text.length() <= 100,
text -> text.chars().allMatch(Character::isLetter)
);
Composition Styles
Standard functional interfaces provide default methods for composing lambdas.
Predicate Composition
Predicate<String> nonEmpty = text -> !text.isBlank();
Predicate<String> shortText = text -> text.length() <= 100;
Predicate<String> valid = nonEmpty.and(shortText);
Predicate<String> invalid = valid.negate();
Function Composition
Function<String, String> trim = text -> text.trim();
Function<String, String> upper = text -> text.toUpperCase();
Function<String, String> normalize = trim.andThen(upper);
Function<String, String> normalizeInReverse = upper.compose(trim);
Consumer Composition
Consumer<String> logStart = text -> System.out.println("Start: " + text);
Consumer<String> logEnd = text -> System.out.println("End: " + text);
Consumer<String> logBoth = logStart.andThen(logEnd);
Explicit Casts for Ambiguous Lambdas
An overloaded method can make a lambda ambiguous. A cast supplies the intended functional-interface type.
void process(Consumer<String> action) { }
void process(Function<String, String> action) { }
// process(text -> System.out.println(text)); // May be ambiguous in overload resolution.
process((Consumer<String>) text -> System.out.println(text));
A cast can also help when a lambda is used inside a conditional expression:
Object action = (Runnable) () -> System.out.println("Run");
Checked Exceptions
A lambda must obey the throws clause of its target functional interface. Runnable and Function do not declare checked exceptions, so checked exceptions must be handled or adapted.
Function<Path, String> read = path -> {
try {
return Files.readString(path);
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
};
A custom interface can declare a checked exception:
@FunctionalInterface
interface FileReader {
String read(Path path) throws IOException;
}
FileReader reader = Files::readString;
A lambda cannot throw a broader checked exception than its functional method allows.
Generics in Lambda Types
Generic target types can infer the lambda parameter and return types.
static <T> T choose(T first, T second, BinaryOperator<T> selector) {
return selector.apply(first, second);
}
String longer = choose("Java", "Lambda", (first, second) ->
first.length() >= second.length() ? first : second);
An explicitly typed lambda can sometimes guide generic inference:
var result = choose(10, 20, (Integer first, Integer second) ->
Math.max(first, second));
Lambda and Anonymous Class Differences
Lambdas and anonymous classes can both implement single-method interfaces, but they are not identical.
| Behavior | Lambda | Anonymous class |
|---|---|---|
this | Refers to the enclosing object. | Refers to the anonymous class instance. |
| Own state | Does not introduce a separate this object. | Can define its own fields and methods. |
| Target type | Requires a functional interface. | Can extend a class or implement an interface. |
| Abstract methods | Target must have one abstract method. | Can implement multiple methods. |
| Checked exceptions | Must match the functional method signature. | Must match overridden method rules. |
| Serialization | Not recommended as a default contract. | Can implement Serializable explicitly. |
Use an anonymous class when you need multiple methods, separate object identity, or custom state. Use a lambda for a focused piece of behavior.
Serialization and Lambda Identity
A lambda is not automatically serializable, even if its target interface looks simple. If serialization is truly required, the target type must extend Serializable.
@FunctionalInterface
interface SerializableAction extends Serializable {
void run();
}
SerializableAction action = () -> System.out.println("Serializable action");
Avoid relying on lambda-generated class names, identity, or serialized representation. They are implementation details and may change between compilations.
Generic Lambda Patterns
Predicate Factory
static <T> Predicate<T> not(Predicate<T> predicate) {
return predicate.negate();
}
Predicate<String> blank = String::isBlank;
Predicate<String> nonBlank = not(blank);
Comparator Factory
static <T, U extends Comparable<? super U>> Comparator<T> by(
Function<T, U> keyExtractor) {
return Comparator.comparing(keyExtractor);
}
List<String> sortedByLength = names.stream()
.sorted(by(String::length))
.toList();
Lazy Evaluation With a Supplier
static <T> T defaultValue(Supplier<T> supplier) {
return supplier.get();
}
String value = defaultValue(() -> loadExpensiveValue());
The supplier is evaluated only when get() is called, which makes it useful for lazy work and fallback values.
Common Mistakes
Forgetting the Target Type
// var increment = number -> number + 1; // Cannot infer a lambda type.
Function<Integer, Integer> increment = number -> number + 1;
Using Braces Without return
// Function<Integer, Integer> square = number -> { number * number; };
Function<Integer, Integer> square = number -> {
return number * number;
};
Accidentally Capturing Mutable State
A lambda can observe mutable fields, which may make concurrent code difficult to reason about.
AtomicInteger counter = new AtomicInteger();
IntUnaryOperator next = value -> counter.incrementAndGet();
Prefer stateless lambdas where possible, especially in parallel streams.
Relying on Side Effects in Streams
// Avoid using shared mutable state inside a stream pipeline.
List<String> result = names.stream()
.map(String::toUpperCase)
.toList();
A lambda used in a stream should generally transform, filter, or combine values instead of mutating external collections.
Confusing map and forEach
Use map to produce transformed values and forEach for terminal side effects.
List<Integer> lengths = names.stream()
.map(String::length)
.toList();
names.forEach(System.out::println);
Version Notes
| Java version | Lambda-related capability |
|---|---|
| Java 8 | Lambda expressions, method references, functional interfaces, default interface methods, and stream APIs. |
| Java 9 | Improved APIs around collections and streams; lambda syntax itself remained unchanged. |
| Java 10 | Local-variable type inference with var, but lambda parameters still could not use var. |
| Java 11 | var became valid in lambda parameters, including annotated var parameters. |
| Java 14 and later | Pattern matching and other language features improved code commonly used inside lambda bodies, but did not change core lambda syntax. |
| Modern Java | The Java 8 lambda model remains the foundation; use current language features inside lambdas according to the project source level. |
Quick Syntax Reference
// No parameters
Runnable a = () -> doSomething();
// One inferred parameter
Consumer<String> b = value -> print(value);
// One parameter with parentheses
Consumer<String> c = (value) -> print(value);
// Multiple inferred parameters
BinaryOperator<Integer> d = (left, right) -> left + right;
// Explicit parameter types
BinaryOperator<Integer> e = (Integer left, Integer right) -> left + right;
// var parameters, Java 11+
BinaryOperator<Integer> f = (var left, var right) -> left + right;
// Expression body
Function<Integer, Integer> g = value -> value * value;
// Block body with return
Function<Integer, Integer> h = value -> {
int result = value * value;
return result;
};
// Block body without return
Consumer<Integer> i = value -> {
System.out.println(value);
System.out.println(value * value);
};
// Method reference
Function<String, Integer> j = String::length;
// Constructor reference
Supplier<List<String>> k = ArrayList::new;
The main rule is simple: choose the shortest lambda that remains clear, give it the correct functional-interface target, and use a block only when the extra statements genuinely improve the code.