Skip to content

Chapter 5 of 8

Functional Programming and the Streams API

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))).

All chapters
  1. 1Java Platform and Memory Model
  2. 2Core Language Features
  3. 3Object-Oriented Programming
  4. 4Generics and the Collections Framework
  5. 5Functional Programming and the Streams API
  6. 6Concurrency and the Java Memory Model
  7. 7Exception Handling, I/O, and Serialization
  8. 8Modern Java, Design Patterns, and Best Practices

Drill it

Reading is not remembering. These come from the Java Programming deck:

Q

What is the JVM?

The Java Virtual Machine (JVM) is an abstract computing machine that executes Java bytecode. It provides platform independence by translating bytecode into nati...

Q

What is the difference between JDK, JRE, and JVM?

JDK (Java Development Kit) includes tools for developing Java programs.JRE (Java Runtime Environment) provides libraries and JVM to run Java applications.JVM (J...

Q

What are the four pillars of OOP in Java?

Encapsulation – bundling data and methods, restricting direct accessInheritance – a class derives from another classPolymorphism – one interface, many implement...

Q

What is encapsulation in Java?

Encapsulation is the practice of keeping fields private and providing access through public getter and setter methods. This protects the internal state of an ob...