- Master the different lambda syntax variations
- Understand when to use expression vs block body
- Learn parameter type inference rules
- Apply best practices for readable lambdas
Lambda Syntax and Variations
You've seen basic lambda syntax. Time to explore all the variations—from verbose to ultra-concise, single-line to multi-statement. Knowing which form to use makes your code clearer.
Full Lambda Syntax
The general form of a lambda expression:
(parameters) -> expression
// or
(parameters) -> { statements; }
The three parts:
- Parameter list in parentheses
- Arrow token
-> - Body - either a single expression or a block of statements
Parameter Variations
No Parameters
Use empty parentheses:
Runnable r = () -> System.out.println("Hello!");
Supplier<Double> random = () -> Math.random();
One Parameter
Parentheses are optional for a single parameter:
// With parentheses
Consumer<String> c1 = (s) -> System.out.println(s);
// Without parentheses (preferred for single param)
Consumer<String> c2 = s -> System.out.println(s);
Multiple Parameters
Parentheses are required:
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
Comparator<String> comp = (s1, s2) -> s1.compareTo(s2);
With Explicit Types
Usually types are inferred, but you can specify them:
// Type inference (preferred)
BiFunction<String, String, Integer> compare = (a, b) -> a.compareTo(b);
// Explicit types
BiFunction<String, String, Integer> compare2 = (String a, String b) -> a.compareTo(b);
Rule: If you specify types for one parameter, you must specify them for all.
// INVALID: Can't mix typed and untyped
// (String a, b) -> a.compareTo(b); // Compilation error!
Body Variations
Expression Body
For simple operations, use a single expression (no braces, no return):
// The expression's value is automatically returned
Function<Integer, Integer> square = x -> x * x;
Predicate<String> isEmpty = s -> s.isEmpty();
BiFunction<Integer, Integer, Integer> max = (a, b) -> Math.max(a, b);
Block Body
For multiple statements, use braces and explicit return (if needed):
Function<Integer, Integer> factorial = n -> {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result; // Explicit return required
};
Consumer<String> logger = message -> {
String timestamp = LocalDateTime.now().toString();
System.out.println(timestamp + ": " + message);
// No return needed for void methods
};
Return Statement Rules
Expression Body
- No
returnkeyword - The expression value is implicitly returned
// Correct: implicit return
Function<Integer, Integer> double_ = x -> x * 2;
// WRONG: can't use return with expression body
// Function<Integer, Integer> wrong = x -> return x * 2; // Error!
Block Body
- Must use
returnfor non-void methods - No
returnfor void methods
// Correct: explicit return in block
Function<Integer, Integer> triple = x -> {
return x * 3;
};
// Correct: no return for void
Consumer<String> printer = s -> {
System.out.println(s);
};
Type Inference
Java infers lambda parameter types from context:
// Type inferred from Comparator<String>
Comparator<String> comp = (a, b) -> a.length() - b.length();
// Type inferred from List<String>.forEach(Consumer<String>)
List<String> names = Arrays.asList("Alice", "Bob");
names.forEach(name -> System.out.println(name)); // name is String
// Type inferred from method parameter
public void process(Predicate<Integer> p) { ... }
process(n -> n > 0); // n is Integer
Syntax Comparison Table
| Scenario | Lambda Syntax |
|---|---|
| No params, expression | () -> expression |
| No params, block | () -> { statements; } |
| One param, expression | x -> expression |
| One param, block | x -> { statements; } |
| Multiple params, expression | (a, b) -> expression |
| Multiple params, block | (a, b) -> { statements; } |
| Typed params | (Type a, Type b) -> expression |
Common Patterns
Transformation
// Simple transformation
Function<String, Integer> length = s -> s.length();
Function<String, String> upper = s -> s.toUpperCase();
// Chained transformation
Function<String, String> process = s -> s.trim().toLowerCase();
Filtering
// Simple predicate
Predicate<Integer> isPositive = n -> n > 0;
Predicate<String> notEmpty = s -> !s.isEmpty();
// Complex predicate
Predicate<Person> isAdult = p -> p.getAge() >= 18 && p.hasId();
Combining
// Binary operations
BinaryOperator<Integer> add = (a, b) -> a + b;
BinaryOperator<String> concat = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
Side Effects
// Consumer for side effects
Consumer<String> log = msg -> System.out.println("[LOG] " + msg);
Consumer<File> delete = f -> f.delete();
Best Practices
1. Keep Lambdas Short
If a lambda is more than 2-3 lines, consider a method reference or a named method:
// Too long - hard to read
list.forEach(item -> {
validate(item);
transform(item);
save(item);
notify(item);
});
// Better - extract to method
list.forEach(this::processItem);
private void processItem(Item item) {
validate(item);
transform(item);
save(item);
notify(item);
}
2. Use Meaningful Parameter Names
// Poor - unclear meaning
list.sort((a, b) -> a.getValue() - b.getValue());
// Better - descriptive names
list.sort((first, second) -> first.getValue() - second.getValue());
// Or even more specific
products.sort((product1, product2) -> product1.getPrice() - product2.getPrice());
3. Prefer Expression Bodies
When possible, use expression form over block form:
// Verbose
Predicate<String> notEmpty = s -> {
return !s.isEmpty();
};
// Concise
Predicate<String> notEmpty = s -> !s.isEmpty();
4. Avoid Complex Logic
// Too complex for a lambda
list.stream()
.filter(item -> {
if (item.getType() == Type.A) {
return item.getValue() > 10;
} else if (item.getType() == Type.B) {
return item.getValue() > 20;
}
return false;
});
// Better - extract the logic
list.stream()
.filter(this::meetsThreshold);
private boolean meetsThreshold(Item item) {
if (item.getType() == Type.A) {
return item.getValue() > 10;
} else if (item.getType() == Type.B) {
return item.getValue() > 20;
}
return false;
}
Lambda syntax is flexible: parentheses optional for single param, types usually inferred, braces optional for single expressions, return implicit for expressions. Use the most concise form that's still readable. Omit parentheses for single parameters. Prefer expression body over block. Let the compiler infer types. Keep lambdas short and focused.
