The arrival of lambdas in Java 8 transformed the language from verbose anonymous classes into concise functional expressions. A lambda implements a functional interface—an interface with exactly one abstract method (SAM)—using the syntax (parameters) -> expression. Common built-in functional interfaces include Predicate<T> (T \(\to\) boolean), Function<T,R> (T \(\to\) R), Consumer<T> (T \(\to\) void), and Supplier<T> (() \(\to\) T). The @FunctionalInterface annotation is informative, not required, but it documents the SAM contract and ensures maintainers do not accidentally break it by adding a second abstract method. Default methods, static methods, and methods inherited from Object do not count toward the SAM count, so interfaces like Predicate<T> (with one abstract test plus a default and(...)) remain functional. Lambdas differ from anonymous inner classes: a lambda targets a SAM type via an invokedynamic call site that often avoids generating an extra class file, while an anonymous inner class creates a fresh named subclass at compile time and can extend a class or implement multiple abstract methods.
Method references are shorthand for lambdas that simply call an existing method. There are four kinds: static references like Math::sqrt (for x \(\to\) Math.sqrt(x)), instance references on a specific object like System.out::println (for x \(\to\) System.out.println(x)), instance references on an arbitrary object of a given type like String::length (for s \(\to\) s.length()), and constructor references like ArrayList::new (for () \(\to\) new ArrayList()). The same thinking underlies CompletableFuture's thenApply vs thenCompose: thenApply is the synchronous map (Function<T, U> \(\to\) CompletableFuture<U>), thenCompose is the asynchronous flatMap (Function<T, CompletionStage<U>> \(\to\) CompletableFuture<U>) that avoids CompletableFuture<CompletableFuture<U>>. Use thenCompose whenever the next step itself returns a future; use thenApply for synchronous value transformations.
The Stream API processes collections of objects in a functional style. A stream is a sequence of elements drawn from a source (a collection, array, generator, or I/O channel), supports lazy evaluation, and does not modify the source. Intermediate operations (filter, map, sorted, distinct, limit) return a new stream and do no work until a terminal operation triggers execution; terminal operations (collect, forEach, reduce, count, findFirst) produce a result or side effect. The map vs flatMap distinction mirrors the same idea in Optional and CompletableFuture: map applies a one-to-one transformation, while flatMap applies a one-to-many transformation that returns a Stream<R> and then flattens it—essential for splitting strings into words, optional unwrapping, or nested collections.
A few of the Stream API's finer points matter in production. findFirst() returns the first element in encounter order, while findAny() returns any element and may be non-deterministic on a parallel stream but is faster in exchange. Collection.stream() processes sequentially on the calling thread; Collection.parallelStream() splits the work across the common ForkJoinPool and only pays off for CPU-bound, stateless operations on large datasets. Spliterator is the internal traversal object that powers streams, reporting characteristics such as ORDERED, DISTINCT, SORTED, SIZED, and IMMUTABLE that help parallelization. The Collectors.toMap(keyMapper, valueMapper) convenience method throws IllegalStateException on duplicate keys and must be configured with a merge function or a custom map supplier; Collectors.joining(delimiter, prefix, suffix) builds a single string via an internal StringJoiner and produces prefix + suffix for empty streams. peek is an intermediate operation intended only for debugging and must not be used for side effects in production; forEach is the terminal counterpart guaranteed to run. Streams integrate with primitives through specialized streams: mapToInt produces an IntStream offering autoboxing-free sum(), average(), summaryStatistics(), and IntStream.range(start, end) (exclusive) vs rangeClosed (inclusive) factories. reduce(identity, accumulator) folds a stream into a single value with an associative operator; collect(supplier, accumulator, combiner) accumulates into a mutable container that can be merged for efficient parallel collection. Because the built-in functional interfaces forbid checked exceptions, lambdas that need to throw IOException cannot compile directly; the standard workarounds are catching and rethrowing as a runtime exception, defining a custom functional interface that declares throws Exception, or using a Lombok @SneakyThrows for the desperate. Java 11 added Predicate.not(p) as a static factory—equivalent to p.negate() but more useful with method references (e.g., list.removeIf(Predicate.not(String::isBlank))).