☕ Java

Stream API

The Stream API, introduced in Java 8, provides a functional, declarative model for processing sequences of elements. A Stream<T> is not a data structure — it carries no storage. It is a pipeline specification: a source that provides elements, zero or more intermediate operations that transform or filter the stream, and exactly one terminal operation that consumes the stream and produces a result or side effect. Streams are lazy: intermediate operations do not execute until the terminal operation is invoked, and execution is fused — elements flow through the entire pipeline one at a time (or in batches for parallel streams), avoiding intermediate collection. Streams are single-use: once a terminal operation has been invoked, the stream is consumed and cannot be reused. Java provides both reference streams (Stream<T>) and primitive streams (IntStream, LongStream, DoubleStream) that avoid boxing overhead. The Stream API covers sources (collections, arrays, files, generators), intermediate operations (filter, map, flatMap, sorted, distinct, limit, skip, peek, mapToInt, mapToObj), terminal operations (forEach, collect, reduce, count, findFirst, findAny, anyMatch, allMatch, noneMatch, toList, min, max), and collectors (toList, toSet, toMap, groupingBy, partitioningBy, joining, counting, summarizing). This entry covers the full lifecycle, every operation class, the short-circuit evaluation, the Spliterator model, collector design, and performance guidance.

Stream Sources, Pipeline Structure, and Lazy Evaluation

A Stream pipeline has three parts: source, intermediate operations, and terminal operation. The source provides elements: Collection.stream() or Collection.parallelStream() for collections; Arrays.stream(array) for arrays; Stream.of(elements) for explicit element lists; IntStream.range(start, end) and IntStream.rangeClosed() for integer ranges; Files.lines(path) for file lines; Stream.generate(Supplier) for infinite generated streams; Stream.iterate(seed, UnaryOperator) for infinite iterated streams; StreamSupport.stream(spliterator, parallel) for custom sources. Intermediate operations are lazy — they register a stage in the pipeline but do not execute any computation until a terminal operation is invoked. They return a new Stream (or IntStream, LongStream, DoubleStream). Intermediate operations are stateless or stateful. Stateless operations (filter, map, flatMap, peek, mapToInt, mapToObj) process each element independently of others. Stateful operations (sorted, distinct, limit, skip) must accumulate state before producing output — sorted sees all elements before emitting the first result; distinct tracks all seen elements; limit and skip track a count. When the terminal operation is invoked, the pipeline is realized. For sequential streams, elements flow through the pipeline one at a time: each element passes through every non-short-circuit stateless operation in sequence before the next element is fetched. Short-circuit operations (limit, findFirst, findAny, anyMatch, allMatch, noneMatch) may stop element consumption before all elements are processed — this is the key performance benefit of streaming over collection-then-iterate patterns. The pipeline fusion means that filter().map().forEach() is not three separate passes over the data. Each element is filtered, then mapped, then consumed in a single traversal. This is equivalent to writing the loop body explicitly, but expressed in composable, declarative form. For large data sets, this avoids creating intermediate collections that would consume memory proportional to the data size.
Java
// ── Stream sources ────────────────────────────────────────────────────
// From Collection:
List<String> names = List.of("Alice", "Bob", "Carol", "Dave");
Stream<String> fromList = names.stream();

// From array:
String[] arr = {"x", "y", "z"};
Stream<String> fromArray = Arrays.stream(arr);

// From explicit elements:
Stream<Integer> ofElements = Stream.of(1, 2, 3, 4, 5);

// Primitive range (no boxing):
IntStream range = IntStream.range(0, 10);       // 0,1,...,9
IntStream rangeClosed = IntStream.rangeClosed(1, 5); // 1,2,3,4,5

// File lines:
try (Stream<String> lines = Files.lines(Path.of("data.txt"), StandardCharsets.UTF_8)) {
    lines.forEach(System.out::println);
}

// Infinite generator:
Stream<Double> randoms = Stream.generate(Math::random);  // infinite — must limit()

// Infinite iterate:
Stream<Integer> powers = Stream.iterate(1, n -> n * 2);  // 1, 2, 4, 8, ...

// Iterate with predicate (Java 9+):
Stream<Integer> bounded = Stream.iterate(1, n -> n <= 100, n -> n * 2); // stops at 64

// ── Pipeline: lazy evaluation demonstration ───────────────────────────
Stream<String> pipeline = names.stream()
    .filter(s -> {
        System.out.println("Filtering: " + s);  // will print
        return s.length() > 3;
    })
    .map(s -> {
        System.out.println("Mapping: " + s);   // will print
        return s.toUpperCase();
    });
// Nothing printed yet — pipeline is lazy, no computation has occurred

System.out.println("Terminal operation starting:");
List<String> result = pipeline.collect(Collectors.toList());
// NOW the filtering and mapping runs:
// Filtering: Alice   Mapping: Alice
// Filtering: Bob     (Bob filtered out — length = 3, not > 3)
// Filtering: Carol   Mapping: Carol
// Filtering: Dave    Mapping: Dave
System.out.println(result);  // [ALICE, CAROL, DAVE]

// ── Short-circuit: stops early ────────────────────────────────────────
Optional<String> firstLong = names.stream()
    .filter(s -> s.length() > 3)  // filter runs for: Alice (pass), Bob (stop here)
    .findFirst();                  // stop after finding first match
// Only "Alice" is processed beyond the filter — "Carol" and "Dave" never seen
System.out.println(firstLong);  // Optional[Alice]

Intermediate Operations — Transforming and Filtering

filter(Predicate<? super T> predicate) keeps elements for which the predicate returns true. It is stateless and lazy. It is the first operation to reach for when reducing the number of elements. map(Function<? super T, ? extends R> mapper) transforms each element to a new value. It is stateless, lazy, and produces a Stream<R>. For transformations to primitive types, use mapToInt(ToIntFunction), mapToLong(ToLongFunction), mapToDouble(ToDoubleFunction) to get primitive streams that avoid boxing. mapToObj(IntFunction) on primitive streams converts back to reference streams. flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) maps each element to a stream and flattens all those streams into a single stream. It is the key operation for one-to-many transformations and for unnesting nested collections. flatMapToInt, flatMapToLong, flatMapToDouble are the primitive variants. sorted() and sorted(Comparator) are stateful — they must buffer all elements before emitting the first. sorted() requires T to implement Comparable. sorted(Comparator) uses the provided comparator. For performance: sort only after filtering and limiting to reduce the number of elements sorted. distinct() is stateful — it tracks all seen elements using equals/hashCode. It removes duplicates. For streams of many elements, distinct() may consume significant memory for the seen-element set. limit(long maxSize) is stateful and short-circuit — it passes at most maxSize elements downstream and then signals upstream to stop producing. skip(long n) is stateful — it discards the first n elements. Together they implement pagination: skip(page * pageSize).limit(pageSize). peek(Consumer<? super T> action) passes each element through unchanged while executing a side effect. It is used for debugging (logging elements mid-pipeline) and should not be used for primary program logic — its execution is not guaranteed for all elements in all situations (short-circuit operations may prevent peek from seeing all elements).
Java
// ── filter: keep elements matching predicate ──────────────────────────
List<String> filtered = names.stream()
    .filter(s -> s.startsWith("A") || s.startsWith("C"))
    .collect(Collectors.toList());
System.out.println(filtered);   // [Alice, Carol]

// ── map: transform each element ───────────────────────────────────────
List<Integer> lengths = names.stream()
    .map(String::length)     // String → Integer (boxed)
    .collect(Collectors.toList());

// mapToInt: avoid boxing ───────────────────────────────────────────────
int totalLength = names.stream()
    .mapToInt(String::length)  // String → int (no boxing)
    .sum();                    // IntStream.sum() — no boxing
System.out.println(totalLength);

// ── flatMap: one-to-many, flatten nested structure ────────────────────
List<List<String>> nested = List.of(
    List.of("a", "b", "c"),
    List.of("d", "e"),
    List.of("f")
);

List<String> flat = nested.stream()
    .flatMap(Collection::stream)  // each inner List → its stream, then flatten
    .collect(Collectors.toList());
System.out.println(flat);  // [a, b, c, d, e, f]

// flatMap for splitting strings into words:
List<String> sentences = List.of("hello world", "foo bar baz");
List<String> words = sentences.stream()
    .flatMap(s -> Arrays.stream(s.split(" ")))
    .collect(Collectors.toList());
System.out.println(words);  // [hello, world, foo, bar, baz]

// ── sorted: stateful, buffers all elements ────────────────────────────
List<String> sortedNames = names.stream()
    .sorted()                          // natural order (Comparable)
    .collect(Collectors.toList());
System.out.println(sortedNames);   // [Alice, Bob, Carol, Dave]

List<String> byLength = names.stream()
    .sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()))
    .collect(Collectors.toList());

// ── distinct: remove duplicates ────────────────────────────────────────
List<Integer> withDups = List.of(1, 2, 2, 3, 3, 3, 4);
List<Integer> deduped = withDups.stream()
    .distinct()
    .collect(Collectors.toList());
System.out.println(deduped);   // [1, 2, 3, 4]

// ── limit and skip: pagination ────────────────────────────────────────
int page = 1, pageSize = 2;
List<String> page1 = names.stream()
    .skip((long) page * pageSize)    // skip page 1 = skip 2 elements
    .limit(pageSize)                 // take next 2
    .collect(Collectors.toList());
System.out.println(page1);   // [Carol, Dave]

// ── peek: debugging (side-effect, don't use for logic) ────────────────
long count = names.stream()
    .peek(s -> System.out.println("Before filter: " + s))
    .filter(s -> s.length() > 3)
    .peek(s -> System.out.println("After filter:  " + s))
    .count();

Terminal Operations and Collectors

Terminal operations trigger pipeline execution and produce a result or a side effect. forEach(Consumer) executes the Consumer for each element — the order is not guaranteed for parallel streams. forEachOrdered(Consumer) guarantees encounter order at the cost of parallelism. count() returns the number of elements as a long. min(Comparator) and max(Comparator) return Optional<T> for the minimum and maximum elements. findFirst() returns Optional<T> for the first element (short-circuit); findAny() returns any element, potentially faster for parallel streams. anyMatch, allMatch, noneMatch take a Predicate and return boolean, short-circuiting when the result is determined. reduce() accumulates elements into a single value. reduce(identity, BinaryOperator) uses an identity value as the starting accumulator and combines each element with the current accumulator. reduce(BinaryOperator) returns Optional<T> for streams that may be empty. reduce(identity, BiFunction<U, T, U>, BinaryOperator<U>) is the three-argument form for parallel streams where the accumulator type differs from the element type. collect(Collector) is the most powerful terminal operation. It accumulates elements into a mutable result container using the Collector's three operations: supplier (creates the container), accumulator (adds an element), and combiner (merges containers from parallel sub-streams). Built-in collectors cover nearly all needs: Collectors.toList(), toSet(), toUnmodifiableList(), toUnmodifiableSet(), toMap(), toConcurrentMap(), groupingBy(), partitioningBy(), joining(), counting(), summingInt/Long/Double(), averagingInt/Long/Double(), summarizingInt/Long/Double(), and toCollection(Supplier). groupingBy(Function<T, K>) groups elements by a classifier function, producing Map<K, List<T>>. groupingBy(Function, Collector) applies a downstream Collector to each group — groupingBy(classifier, Collectors.counting()) produces Map<K, Long>. partitioningBy(Predicate) groups into Map<Boolean, List<T>> — true for matching elements, false for non-matching. joining(delimiter, prefix, suffix) concatenates String elements. toMap(keyMapper, valueMapper) produces Map<K, V>, throwing on duplicate keys; toMap(keyMapper, valueMapper, mergeFunction) handles duplicate keys.
Java
// ── Terminal operations ───────────────────────────────────────────────
long count = names.stream().filter(s -> s.length() > 3).count();
System.out.println(count);   // 3

Optional<String> shortest = names.stream().min(Comparator.comparingInt(String::length));
Optional<String> longest  = names.stream().max(Comparator.comparingInt(String::length));
System.out.println(shortest.orElse(""));  // Bob
System.out.println(longest.orElse(""));   // Carol (or Alice — tied at 5)

boolean anyA = names.stream().anyMatch(s -> s.startsWith("A"));  // true
boolean allA = names.stream().allMatch(s -> s.startsWith("A"));  // false
boolean noneZ = names.stream().noneMatch(s -> s.startsWith("Z")); // true

// reduce: sum lengths
int totalLen = names.stream()
    .mapToInt(String::length)
    .reduce(0, Integer::sum);  // IntStream.reduce(0, IntBinaryOperator)
System.out.println(totalLen);

// reduce with Object accumulator:
String longest2 = names.stream()
    .reduce("", (a, b) -> a.length() >= b.length() ? a : b);
System.out.println(longest2);  // Carol

// ── Collectors: the comprehensive toolkit ─────────────────────────────
// toList (Java 16+): unmodifiable list preserving encounter order:
List<String> immList = names.stream().collect(Collectors.toUnmodifiableList());
// or since Java 16:
List<String> toListResult = names.stream().toList();  // unmodifiable

// toSet:
Set<String> nameSet = names.stream().collect(Collectors.toSet());

// toMap: String → length
Map<String, Integer> nameLengths = names.stream()
    .collect(Collectors.toMap(
        Function.identity(),   // key: the string itself
        String::length         // value: its length
    ));
System.out.println(nameLengths);  // {Alice=5, Bob=3, Carol=5, Dave=4}

// toMap with merge function (handles duplicate keys):
Map<Integer, String> byLength = names.stream()
    .collect(Collectors.toMap(
        String::length,      // key: length
        Function.identity(), // value: name
        (a, b) -> a + "," + b  // merge duplicate keys: "Alice,Carol" for length 5
    ));

// groupingBy: group into Map<K, List<T>>
Map<Integer, List<String>> byLengthGroup = names.stream()
    .collect(Collectors.groupingBy(String::length));
System.out.println(byLengthGroup);
// {3=[Bob], 4=[Dave], 5=[Alice, Carol]}

// groupingBy with downstream collector:
Map<Integer, Long> countByLength = names.stream()
    .collect(Collectors.groupingBy(String::length, Collectors.counting()));
System.out.println(countByLength);  // {3=1, 4=1, 5=2}

Map<Integer, String> joinedByLength = names.stream()
    .collect(Collectors.groupingBy(
        String::length,
        Collectors.joining(", ")  // join names of same length
    ));
System.out.println(joinedByLength);  // {3=Bob, 4=Dave, 5=Alice, Carol}

// partitioningBy: split into true/false
Map<Boolean, List<String>> partition = names.stream()
    .collect(Collectors.partitioningBy(s -> s.length() > 3));
System.out.println(partition.get(true));   // [Alice, Carol, Dave]
System.out.println(partition.get(false));  // [Bob]

// joining: concatenate strings
String joined = names.stream().collect(Collectors.joining(", ", "[", "]"));
System.out.println(joined);  // [Alice, Bob, Carol, Dave]

// summarizingInt: all stats at once
IntSummaryStatistics stats = names.stream()
    .collect(Collectors.summarizingInt(String::length));
System.out.printf("count=%d sum=%d min=%d max=%d avg=%.1f%n",
    stats.getCount(), stats.getSum(), stats.getMin(),
    stats.getMax(), stats.getAverage());
// count=4 sum=17 min=3 max=5 avg=4.3

Related Topics in Java 8 Features

Intermediate Operations
Intermediate operations are Stream API methods that transform one stream into another stream, enabling pipeline construction through method chaining. Every intermediate operation is lazy — it does not process any elements when called; it only builds a description of the computation to be performed. The actual processing occurs only when a terminal operation triggers stream traversal, at which point all intermediate operations execute in a single pass over the data, interleaved element by element rather than stage by stage. This laziness and fusion model is the defining architectural feature of the Streams API, distinguishing it from eager collection-transformation approaches. Intermediate operations fall into categories: filtering (filter, distinct), transformation (map and its primitive variants, flatMap), ordering (sorted), size-limiting (limit, skip), peeking (peek), and the Java 9 additions for prefix-based selection (takeWhile, dropWhile). This entry covers the laziness model and why it matters, every intermediate operation with its exact semantics and performance characteristics, stateless versus stateful intermediate operations, short-circuiting operations and how they interact with infinite streams, the map/flatMap distinction, and operation ordering and its performance implications.
Terminal Operations
Terminal operations are Stream API methods that trigger the actual traversal and processing of a stream pipeline, producing a non-stream result — a value, a collection, a side effect, or nothing (void). A stream can have exactly one terminal operation; once invoked, the stream is consumed and cannot be reused (calling any further operation on it throws IllegalStateException). Terminal operations fall into categories: reduction (reduce, collect), matching (anyMatch, allMatch, noneMatch), finding (findFirst, findAny), counting (count), iteration with side effects (forEach, forEachOrdered), and array/collection materialization (toArray, toList). Some terminal operations are short-circuiting (anyMatch, findFirst) and can terminate before examining the entire stream; others (forEach, count in most cases, collect) must examine every element. This entry covers every terminal operation in depth, the reduce() method and its three overloads with the combiner function's role in parallel execution, the Collectors framework as the primary mechanism for complex terminal reduction, short-circuiting semantics and their interaction with infinite streams, the single-use constraint and its rationale, and the choice between equivalent terminal operations for performance and clarity.
Optional
Optional<T>, introduced in Java 8, is a container object that may or may not hold a non-null value, designed to make the possible absence of a value explicit in a method's type signature rather than implicit through a nullable return type. Optional.of(value) creates a present Optional and throws NullPointerException if value is null; Optional.empty() creates an absent Optional; Optional.ofNullable(value) creates either a present or empty Optional depending on whether value is null. The design intent, stated explicitly in the Javadoc, is as a return type for methods where the absence of a result is a valid and expected outcome, communicating that absence through the type system instead of through null, with the goal of reducing NullPointerException at the API boundary. Optional is explicitly not intended for use as a field type, a method parameter type, or inside collections, and the JDK team has stated that misuse in these contexts works against its design intent. This entry covers the complete Optional API including creation, value extraction, conditional execution, transformation and chaining, the primitive specializations OptionalInt/OptionalLong/OptionalDouble, and the established conventions for where Optional should and should not be used.
Default Methods
Default methods, introduced in Java 8, are methods declared in an interface with a body, marked with the default keyword, providing a concrete implementation that implementing classes inherit automatically without being forced to override it. Before Java 8, interfaces could declare only abstract methods, meaning adding any new method to an interface would break every existing implementation — a problem that became critical when the Streams API needed to add methods like forEach(), stream(), and removeIf() to the existing Collection-family interfaces without breaking the entire ecosystem of third-party collection implementations. Default methods solved this by allowing interface evolution: new methods can be added to an interface with a default implementation, and existing implementing classes continue to compile and work without modification, inheriting the default behavior until they choose to override it. This entry covers the design motivation and interface evolution problem default methods solve, the complete syntax and inheritance rules, the diamond problem and Java's resolution rules for conflicting default methods from multiple interfaces, the explicit disambiguation syntax using InterfaceName.super.method(), the interaction between default methods and abstract classes, and the static method companion feature introduced alongside default methods.