Java Streams API: The Complete Reference Guide

The Streams API, introduced in Java 8, fundamentally changed how Java developers process collections. Instead of writing loops that describe how to iterate, you write pipelines that describe what to compute. The result is code that is shorter, easier to parallelise, and far more composable than the equivalent imperative loop.

This guide is designed as a reference you will return to. Every method in the API is covered with a working example and annotated output. The final sections address collectors in depth, parallel streams, and the mistakes that trip up developers who are new to the functional style.


What Is a Stream?

A Stream<T> is a sequence of elements that supports sequential and parallel aggregate operations. It is not a data structure โ€” it does not store elements. Instead, it wraps a data source (a collection, an array, an I/O channel) and lets you express a chain of transformations called a pipeline.

A stream pipeline has three parts:

  1. Source โ€” a collection, array, generator function, or I/O channel that supplies the elements.
  2. Intermediate operations โ€” zero or more lazy transformations (filter, map, sorted โ€ฆ) that return another stream.
  3. Terminal operation โ€” exactly one eager operation (collect, forEach, reduce โ€ฆ) that triggers the pipeline and produces a result or side effect.

Streams are lazy: intermediate operations are not executed until a terminal operation is invoked. They are also single-use: once a terminal operation has run, the stream is consumed and cannot be reused.


1. Creating Streams

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class StreamCreation {
    public static void main(String[] args) {

        // 1. From a Collection (most common)
        List names = List.of("Alice", "Bob", "Carol");
        Stream fromList = names.stream();

        // 2. From varargs / inline values
        Stream fromOf = Stream.of(1, 2, 3, 4, 5);

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

        // 4. Primitive streams (avoid boxing overhead)
        IntStream range      = IntStream.range(1, 6);      // 1, 2, 3, 4, 5
        IntStream rangeClosed = IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5

        // 5. Infinite stream with generate()
        Stream randoms = Stream.generate(Math::random).limit(3);

        // 6. Infinite stream with iterate()
        Stream evens = Stream.iterate(0, n -> n + 2).limit(5);
        evens.forEach(System.out::println);
        // Output: 0  2  4  6  8
    }
}

2. Intermediate Operations

Intermediate operations are lazy and return a new stream. They can be chained in any order, but the order matters for performance: put the cheapest, most selective filters first.

filter() โ€” keep elements matching a predicate

List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Keep only even numbers
List evens = numbers.stream()
        .filter(n -> n % 2 == 0)   // predicate: true if even
        .toList();                  // terminal: collect to List (Java 16+)

System.out.println(evens);
// Output: [2, 4, 6, 8, 10]

map() โ€” transform each element

List words = List.of("hello", "world", "java");

// Convert each word to uppercase
List upper = words.stream()
        .map(String::toUpperCase)   // method reference to String.toUpperCase()
        .toList();

System.out.println(upper);
// Output: [HELLO, WORLD, JAVA]

flatMap() โ€” flatten nested structures

flatMap maps each element to a stream, then flattens all resulting streams into one. The classic use case is a list of lists.

List> nested = List.of(
        List.of(1, 2, 3),
        List.of(4, 5),
        List.of(6, 7, 8, 9)
);

// Flatten into a single stream of integers
List flat = nested.stream()
        .flatMap(List::stream)      // each inner List becomes a stream; flatMap merges them
        .toList();

System.out.println(flat);
// Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

sorted() โ€” order elements

List names = List.of("Carol", "Alice", "Bob", "Dave");

// Natural order
List asc  = names.stream().sorted().toList();
System.out.println(asc);   // [Alice, Bob, Carol, Dave]

// Reverse order via Comparator
List desc = names.stream()
        .sorted(Comparator.reverseOrder())
        .toList();
System.out.println(desc);  // [Dave, Carol, Bob, Alice]

// Sort by string length, then alphabetically
List byLen = names.stream()
        .sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()))
        .toList();
System.out.println(byLen); // [Bob, Alice, Carol, Dave]

distinct(), limit(), skip()

List nums = List.of(1, 2, 2, 3, 3, 3, 4, 5);

// distinct() removes duplicates (uses equals())
System.out.println(nums.stream().distinct().toList());
// Output: [1, 2, 3, 4, 5]

// limit(n) truncates to the first n elements
System.out.println(nums.stream().limit(4).toList());
// Output: [1, 2, 2, 3]

// skip(n) discards the first n elements
System.out.println(nums.stream().skip(5).toList());
// Output: [3, 4, 5]

peek() โ€” observe without modifying

peek is an intermediate operation that applies a Consumer to each element as it flows through the pipeline. Its primary use is debugging; never rely on it for side effects in production code.

List result = List.of(1, 2, 3, 4, 5).stream()
        .filter(n -> n % 2 != 0)
        .peek(n -> System.out.println("After filter: " + n))   // debug observer
        .map(n -> n * n)
        .peek(n -> System.out.println("After map   : " + n))
        .toList();

// Output:
// After filter: 1
// After map   : 1
// After filter: 3
// After map   : 9
// After filter: 5
// After map   : 25

3. Terminal Operations

collect() โ€” gather into a collection

import java.util.stream.Collectors;

List names = List.of("Alice", "Bob", "Carol", "Dave", "Alice");

// Collect to a mutable ArrayList
List list = names.stream().collect(Collectors.toList());

// Collect to an unmodifiable List (Java 10+)
List unmodifiable = names.stream().collect(Collectors.toUnmodifiableList());

// Collect to a Set (removes duplicates)
var set = names.stream().collect(Collectors.toSet());

// Join with a delimiter
String joined = names.stream()
        .distinct()
        .collect(Collectors.joining(", ", "[", "]"));
System.out.println(joined);
// Output: [Alice, Bob, Carol, Dave]

// Group by first letter
var grouped = names.stream()
        .collect(Collectors.groupingBy(s -> s.charAt(0)));
System.out.println(grouped);
// Output: {A=[Alice, Alice], B=[Bob], C=[Carol], D=[Dave]}

reduce() โ€” fold elements into one value

List nums = List.of(1, 2, 3, 4, 5);

// Sum with an identity value (returns int, not Optional)
int sum = nums.stream()
        .reduce(0, Integer::sum);   // 0 is the identity; Integer::sum is the accumulator
System.out.println(sum);  // 15

// Product without identity (returns Optional)
var product = nums.stream()
        .reduce((a, b) -> a * b);
product.ifPresent(System.out::println);  // 120

// Concatenate strings
String sentence = Stream.of("Java", "Streams", "are", "powerful")
        .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
System.out.println(sentence);  // Java Streams are powerful

Matching and finding

List data = List.of(3, 7, 1, 9, 4, 6);

// anyMatch โ€” returns true if at least one element matches
boolean hasEven = data.stream().anyMatch(n -> n % 2 == 0);
System.out.println(hasEven);   // true

// allMatch โ€” returns true if every element matches
boolean allPositive = data.stream().allMatch(n -> n > 0);
System.out.println(allPositive); // true

// noneMatch โ€” returns true if no element matches
boolean noneNegative = data.stream().noneMatch(n -> n < 0);
System.out.println(noneNegative); // true

// findFirst โ€” returns the first element as Optional
data.stream()
    .filter(n -> n > 5)
    .findFirst()
    .ifPresent(System.out::println); // 7

// count, min, max
System.out.println(data.stream().count());               // 6
System.out.println(data.stream().min(Integer::compare)); // Optional[1]
System.out.println(data.stream().max(Integer::compare)); // Optional[9]

4. Collectors in Depth

import java.util.stream.Collectors;
import java.util.*;

record Person(String name, String city, int age) {}

List people = List.of(
    new Person("Alice",   "London",  30),
    new Person("Bob",     "London",  25),
    new Person("Carol",   "Paris",   35),
    new Person("Dave",    "Paris",   28),
    new Person("Eve",     "London",  22)
);

// --- groupingBy: group people by city ---
Map> byCity = people.stream()
        .collect(Collectors.groupingBy(Person::city));
// {London=[Alice, Bob, Eve], Paris=[Carol, Dave]}

// --- groupingBy + counting: how many people per city ---
Map countByCity = people.stream()
        .collect(Collectors.groupingBy(Person::city, Collectors.counting()));
System.out.println(countByCity);
// {London=3, Paris=2}

// --- groupingBy + averaging: average age per city ---
Map avgAgeByCity = people.stream()
        .collect(Collectors.groupingBy(Person::city,
                 Collectors.averagingInt(Person::age)));
System.out.println(avgAgeByCity);
// {London=25.666..., Paris=31.5}

// --- partitioningBy: split into two groups (true/false) ---
Map> over28 = people.stream()
        .collect(Collectors.partitioningBy(p -> p.age() > 28));
System.out.println(over28.get(true));   // [Alice, Carol]
System.out.println(over28.get(false));  // [Bob, Dave, Eve]

// --- toMap: city -> oldest person in that city ---
Map oldestByCity = people.stream()
        .collect(Collectors.toMap(
                Person::city,
                Person::name,
                (existing, incoming) -> existing  // merge function: keep first
        ));
System.out.println(oldestByCity); // {London=Alice, Paris=Carol}

5. Primitive Streams: IntStream, LongStream, DoubleStream

Storing primitives in a Stream<Integer> forces boxing/unboxing on every element. For numeric work, use the specialised primitive streams instead โ€” they are significantly faster for large data sets.

// IntStream.range produces an int sequence without boxing
int sumOf1to100 = IntStream.rangeClosed(1, 100).sum();
System.out.println(sumOf1to100);   // 5050

// Statistics in one pass
IntSummaryStatistics stats = IntStream.of(3, 1, 4, 1, 5, 9, 2, 6)
        .summaryStatistics();
System.out.println("Min   : " + stats.getMin());    // 1
System.out.println("Max   : " + stats.getMax());    // 9
System.out.println("Sum   : " + stats.getSum());    // 31
System.out.println("Avg   : " + stats.getAverage()); // 3.875
System.out.println("Count : " + stats.getCount());  // 8

// Map a Stream to an IntStream to avoid boxing
List words = List.of("hello", "world", "streams");
int totalChars = words.stream()
        .mapToInt(String::length)   // Stream -> IntStream (no boxing)
        .sum();
System.out.println(totalChars);  // 19

6. Parallel Streams

Switching a sequential stream to parallel requires only changing .stream() to .parallelStream() (or calling .parallel() on any existing stream). The framework splits the source, processes chunks concurrently on the common ForkJoinPool, and merges the results.

// Parallel sum โ€” safe because addition is associative and stateless
long count = 1_000_000L;
long parallelSum = LongStream.rangeClosed(1, count)
        .parallel()
        .sum();
System.out.println(parallelSum);  // 500000500000

// WARNING: parallel streams are NOT always faster.
// Parallelism pays off only when:
//   1. The data source is large (tens of thousands of elements minimum)
//   2. The work per element is non-trivial (avoid for simple filter/map)
//   3. The pipeline is stateless and the operations are order-independent
//   4. No shared mutable state is accessed inside the lambda

7. Common Pitfalls

PitfallWhat goes wrongFix
Reusing a streamIllegalStateException: stream has already been operated uponCreate a new stream from the source each time
Mutating state inside a lambdaRace conditions in parallel; undefined behaviour in sequentialUse stateless lambdas; collect into new objects rather than modifying existing ones
Calling .collect(Collectors.toList()) when .toList() (Java 16+) sufficesVerbose, returns a mutable list unnecessarilyPrefer .toList() for read-only results
Using forEach to build a collectionDefeats laziness, breaks parallelism, order not guaranteedUse collect() instead of mutating an external list inside forEach
Over-using peek for side effectspeek may not fire on all elements if the pipeline short-circuitsUse peek only for debugging; use forEach or map for real side effects / transforms
Parallel streams on small dataThread-pool overhead makes it slower than sequentialBenchmark; only parallelise if data set is large and work per element is heavy

Quick-Reference: Every Operation at a Glance

OperationTypeReturnsNotes
filter(Predicate)IntermediateStream<T>Keeps elements matching predicate
map(Function)IntermediateStream<R>1-to-1 transform
flatMap(Function)IntermediateStream<R>1-to-many transform, then flatten
distinct()IntermediateStream<T>Uses equals()
sorted()IntermediateStream<T>Natural or custom Comparator
limit(n)IntermediateStream<T>First n elements
skip(n)IntermediateStream<T>Drop first n elements
peek(Consumer)IntermediateStream<T>Debug observer
collect(Collector)TerminalRMost versatile terminal op
toList()TerminalList<T>Java 16+; unmodifiable
reduce(identity, BinaryOp)TerminalTFold to single value
forEach(Consumer)TerminalvoidSide effects; order not guaranteed in parallel
count()Terminallong
min/max(Comparator)TerminalOptional<T>
anyMatch/allMatch/noneMatchTerminalbooleanShort-circuit
findFirst/findAnyTerminalOptional<T>findAny may be faster in parallel

See Also

Conclusion

The Streams API is built around three ideas: laziness (work is deferred until a terminal operation triggers the pipeline), composability (intermediate operations chain cleanly without intermediate allocations), and optionality of parallelism (add .parallel() and the framework handles the thread management). Fluency with filter, map, flatMap, and the collector library covers the vast majority of real-world use cases. The edge cases โ€” primitive streams for performance, reduce for custom aggregations, partitioningBy and groupingBy for analytics โ€” are what separate developers who know the API from developers who truly understand it.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.