Lambdas, Functional Interfaces, Streams, and Optional
Use Java's functional programming features effectively with lambdas, method references, streams, collectors, parallel streams, and Optional.
Lambdas and Functional Interfaces
A lambda is a compact implementation of a functional interface: an interface with one abstract method.
Predicate<String> nonEmpty = value -> !value.isBlank();
Function<String, Integer> length = String::length;
Consumer<String> log = System.out::println;
Common functional interfaces include Predicate<T>, Function<T,R>, Consumer<T>, and Supplier<T>. Primitive-specialized forms can reduce boxing in hot numeric code.
A lambda should generally be small. Complex behavior is often clearer as a named method.
- A lambda targets a functional interface.
- Method references can improve readability.
- Prefer small, intention-revealing lambdas.
Stream Pipelines
Streams provide a declarative way to process sequences. A pipeline typically has a source, intermediate operations, and a terminal operation.
List<String> names = List.of("Java", "Kotlin", "JavaScript");
List<String> result = names.stream()
.filter(name -> name.startsWith("J"))
.map(String::toUpperCase)
.toList();
Intermediate operations such as filter, map, sorted, and distinct are generally lazy. A terminal operation such as toList, count, findFirst, or forEach drives execution.
Streams are not collections and are usually single-use.
Calling filter() or map() alone does not process elements. A terminal operation drives the pipeline.
- Streams are pipelines, not storage containers.
- Intermediate operations are generally lazy.
- A stream should normally be consumed once.
Collectors, Grouping, and Reduction
Collectors turn streams into lists, sets, maps, grouped structures, and summaries.
Map<Integer, List<String>> byLength =
names.stream()
.collect(Collectors.groupingBy(String::length));
int totalLength =
names.stream()
.mapToInt(String::length)
.sum();
groupingBy is useful for classification. partitioningBy splits values into two groups. Reduction operations combine elements into a result.
Prefer built-in collectors when they express the operation clearly. Custom collectors require careful supplier, accumulator, combiner, and finisher design.
- Use
groupingByfor classification and aggregation. - Primitive streams can reduce boxing for numeric operations.
- Custom collectors must account for sequential and parallel semantics.
Optional and Absence Modeling
Optional<T> models a value that may be absent. It is most useful as a return type when absence is a meaningful outcome.
Optional<String> username = findUsername();
String display = username
.map(String::trim)
.filter(value -> !value.isEmpty())
.orElse("anonymous");
Avoid using Optional as a field or parameter by default. orElse evaluates its fallback immediately, while orElseGet evaluates a supplier lazily.
Avoid get() without first establishing presence.
Use Optional deliberately at API boundaries where absence is part of the contract.
- Use Optional to model meaningful absence.
orElseGetis lazy;orElseevaluates its argument eagerly.- Avoid unsafe
Optional.get()patterns.