Working with Lambda Expressions
1. Creating Lambda Syntax
| Form | Syntax | Use Case |
|---|---|---|
| Zero params | () -> expr | Runnable, Supplier |
| Single param | x -> expr | Function, Consumer |
| Multi params | (a, b) -> expr | BiFunction, Comparator |
| Typed params | (int a, int b) -> a + b | Disambiguation |
| Block body | (x) -> { stmt; return v; } | Multi-statement |
| var params Java 11+ | (@NotNull var x) -> x | Annotated lambda params |
Example: Common lambda forms
Runnable r = () -> System.out.println("run");
Function<String, Integer> len = s -> s.length();
Comparator<User> byAge = (a, b) -> Integer.compare(a.age(), b.age());
BinaryOperator<Integer> sum = (a, b) -> { int t = a + b; return t; };
2. Using Functional Interfaces (@FunctionalInterface)
| Interface | Signature | Purpose |
|---|---|---|
Function<T,R> | R apply(T) | Transform |
BiFunction<T,U,R> | R apply(T,U) | Two-arg transform |
Predicate<T> | boolean test(T) | Filter/match |
Consumer<T> | void accept(T) | Side effect |
Supplier<T> | T get() | Lazy producer |
UnaryOperator<T> | T apply(T) | Same-type transform |
BinaryOperator<T> | T apply(T,T) | Reduce/combine |
IntFunction<R> | R apply(int) | Primitive specialization |
Example: Custom functional interface
@FunctionalInterface
interface ThrowingFunction<T, R, E extends Exception> {
R apply(T t) throws E;
}
ThrowingFunction<String, Integer, IOException> parser = Files::size;
3. Understanding Method References (::)
| Kind | Syntax | Equivalent Lambda |
|---|---|---|
| Static | Integer::parseInt | s -> Integer.parseInt(s) |
| Bound instance | System.out::println | x -> System.out.println(x) |
| Unbound instance | String::length | s -> s.length() |
| Constructor | ArrayList::new | () -> new ArrayList<>() |
| Array constructor | String[]::new | n -> new String[n] |
4. Using Constructor References (Class::new)
| Target Type | Reference | Calls |
|---|---|---|
Supplier<User> | User::new | new User() |
Function<String,User> | User::new | new User(name) |
BiFunction<String,Integer,User> | User::new | new User(name,age) |
IntFunction<int[]> | int[]::new | new int[n] |
Example: Stream toArray with constructor ref
String[] arr = list.stream().map(String::toUpperCase).toArray(String[]::new);
List<User> users = names.stream().map(User::new).toList();
5. Capturing Variables
| Variable Type | Capture Rule | Notes |
|---|---|---|
| Local | Must be effectively final | Compile error otherwise |
| Instance field | Captured via this | Mutation allowed |
| Static field | Captured by class | Mutation allowed |
| Method param | Effectively final | Same as locals |
Warning: Capturing mutable references (e.g.,
AtomicInteger) sidesteps the rule but introduces shared state — use cautiously in parallel streams.6. Understanding Lambda Scope
| Aspect | Lambda | Anonymous Class |
|---|---|---|
this | Enclosing instance | Anonymous instance |
| Shadowing | Cannot shadow enclosing names | Can shadow |
| Scope | Lexical (enclosing method) | New scope |
| Bytecode | invokedynamic + LambdaMetafactory | Generated .class file |
7. Handling Exceptions in Lambdas
| Pattern | Approach | Trade-off |
|---|---|---|
| Wrap as runtime | try/catch + throw new RuntimeException | Loses checked semantics |
| Throwing FI | Custom @FunctionalInterface with throws | Type-safe propagation |
| Sneaky throws | Generic trick to throw checked | Hides exceptions |
| Result/Either | Return wrapper instead of throwing | Functional, verbose |
Example: Wrapper utility
static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R, ?> f) {
return t -> { try { return f.apply(t); }
catch (Exception e) { throw new RuntimeException(e); } };
}
paths.stream().map(unchecked(Files::readString)).toList();
8. Using Lambda with Collections
| Method | Signature | Use |
|---|---|---|
forEach | Consumer<T> | Iterate |
removeIf | Predicate<T> | Conditional removal |
replaceAll | UnaryOperator<T> | In-place transform |
Map.compute | BiFunction<K,V,V> | Atomic update |
Map.merge | BiFunction<V,V,V> | Combine on key collision |
sort | Comparator<T> | In-place sort |
9. Creating Custom Functional Interfaces
| Rule | Detail |
|---|---|
| Single abstract method | Required (SAM) |
default methods | Allowed, unlimited |
static methods | Allowed |
Object overrides | Don't count toward SAM |
@FunctionalInterface | Optional, enforces SAM |
Example: Tri-function
@FunctionalInterface
interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
default <V> TriFunction<A, B, C, V> andThen(Function<? super R, ? extends V> after) {
return (a, b, c) -> after.apply(apply(a, b, c));
}
}
10. Composing Functions
| Method | Order | Example |
|---|---|---|
andThen | this → after | f.andThen(g).apply(x) == g(f(x)) |
compose | before → this | f.compose(g).apply(x) == f(g(x)) |
Function.identity() | x → x | Reduce starting value |
Example: Pipeline composition
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, Integer> len = String::length;
Function<String, Integer> pipeline = trim.andThen(upper).andThen(len);
pipeline.apply(" hello "); // 5
11. Using Predicate Composition
| Method | Logic | Use |
|---|---|---|
and | Short-circuit AND | Combine filters |
or | Short-circuit OR | Match any |
negate | Logical NOT | Invert |
Predicate.not(p) Java 11+ | Static negation | Method ref friendly |
Predicate.isEqual(o) | Objects.equals | Equality test |
Example: Filter chain
Predicate<User> adult = u -> u.age() >= 18;
Predicate<User> active = User::isActive;
users.stream().filter(adult.and(active).and(Predicate.not(User::isBanned))).toList();
12. Understanding Lambda Performance Characteristics
| Aspect | Behavior | Note |
|---|---|---|
| Bootstrap | invokedynamic + LambdaMetafactory | One-time per call site |
| Non-capturing | Singleton instance reused | Zero allocation |
| Capturing | New instance per invocation | Avoid in hot loops |
| Method ref | Same as lambda | Often inlined by JIT |
| Boxing | Generic FIs box primitives | Use IntFunction etc. |
Note: Modern HotSpot JIT inlines small lambdas at megamorphic call sites; benchmark before optimizing.