Streams in Java

Published on June 18, 2026

A Stream is not a data structure — it’s a pipeline. You describe what you want, and the stream figures out how to compute it lazily.


What is a Stream?

A Stream<T> is a sequence of elements that supports a pipeline of operations. The key properties:

  • Not a data structure — it doesn’t store elements. It pulls them from a source (collection, array, generator).
  • Lazy — intermediate operations are not executed until a terminal operation is called.
  • Single-use — once consumed, a stream cannot be reused.

A pipeline has three parts:

source → intermediate operations (lazy) → terminal operation (triggers execution)

Creating streams

// From a collection
List<String> names = List.of("Alice", "Bob", "Charlie");
Stream<String> s = names.stream();

// From an array
int[] arr = {1, 2, 3};
IntStream s = Arrays.stream(arr);

// From values directly
Stream<String> s = Stream.of("a", "b", "c");

// Infinite stream — generate
Stream<Double> randoms = Stream.generate(Math::random);

// Infinite stream — iterate (like a for loop)
Stream<Integer> naturals = Stream.iterate(0, n -> n + 1); // 0, 1, 2, 3, ...

// Finite iterate (Java 9+)
Stream<Integer> first10 = Stream.iterate(0, n -> n < 10, n -> n + 1);

Intermediate operations

These are lazy — they return a new stream and don’t execute until a terminal operation is called.

filter — keep elements matching a predicate

List<Integer> evens = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

map — transform each element

List<String> upperNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

// Map to a different type
List<Integer> lengths = names.stream()
    .map(String::length)
    .collect(Collectors.toList());

flatMap — flatten a stream of streams

Use when each element maps to multiple elements. map would give you a Stream<List<T>>; flatMap flattens it to Stream<T>.

List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4), List.of(5));

// map gives Stream<List<Integer>> — not what we want
// flatMap gives Stream<Integer>
List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());
// [1, 2, 3, 4, 5]

Another common use — splitting strings into words:

List<String> sentences = List.of("hello world", "foo bar");
List<String> words = sentences.stream()
    .flatMap(s -> Arrays.stream(s.split(" ")))
    .collect(Collectors.toList());
// ["hello", "world", "foo", "bar"]

sorted — sort elements

// Natural order
list.stream().sorted()

// Custom comparator
list.stream().sorted(Comparator.comparingInt(Student::getAge))

// Descending
list.stream().sorted(Comparator.comparingInt(Student::getAge).reversed())

distinct — remove duplicates

List<Integer> unique = List.of(1, 2, 2, 3, 3, 3).stream()
    .distinct()
    .collect(Collectors.toList());
// [1, 2, 3]

limit and skip

// First 5 elements
stream.limit(5)

// Skip first 5, take the rest
stream.skip(5)

// Pagination: page 2, size 10
stream.skip(10).limit(10)

peek — inspect elements without consuming the stream

Useful for debugging. Does not alter the stream.

list.stream()
    .filter(n -> n > 0)
    .peek(n -> System.out.println("after filter: " + n))
    .map(n -> n * 2)
    .collect(Collectors.toList());

Terminal operations

These trigger the pipeline to execute and produce a result. After calling one, the stream is consumed.

collect — gather results into a collection

The most common terminal operation. Takes a Collector — covered in the next section.

List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());

forEach — consume each element

names.stream().forEach(System.out::println);

Note: forEach does not guarantee order on parallel streams. Use forEachOrdered if order matters.

count

long count = names.stream().filter(n -> n.startsWith("A")).count();

findFirst and findAny

Both return an Optional<T>. findFirst returns the first element in encounter order; findAny may return any element (faster on parallel streams).

Optional<String> first = names.stream()
    .filter(n -> n.length() > 3)
    .findFirst();

first.ifPresent(System.out::println);
String val = first.orElse("none");

anyMatch, allMatch, noneMatch

Short-circuit — stop as soon as the answer is known.

boolean hasAdult = students.stream().anyMatch(s -> s.getAge() >= 18);
boolean allAdults = students.stream().allMatch(s -> s.getAge() >= 18);
boolean noneMinor = students.stream().noneMatch(s -> s.getAge() < 18);

min and max

Return Optional<T>.

Optional<Student> youngest = students.stream()
    .min(Comparator.comparingInt(Student::getAge));

Optional<Integer> maxVal = numbers.stream().max(Integer::compare);

reduce — fold elements into a single value

// Sum
int sum = numbers.stream().reduce(0, Integer::sum);

// Product
int product = numbers.stream().reduce(1, (a, b) -> a * b);

// Without identity — returns Optional
Optional<Integer> max = numbers.stream().reduce(Integer::max);

Collectors

Collectors is a utility class with factory methods for common collection strategies.

toList, toSet, toUnmodifiableList

List<String> list = stream.collect(Collectors.toList());
Set<String> set  = stream.collect(Collectors.toSet());
List<String> immutable = stream.collect(Collectors.toUnmodifiableList());

toMap

// Map from name to age
Map<String, Integer> nameToAge = students.stream()
    .collect(Collectors.toMap(Student::getName, Student::getAge));

// Handle duplicate keys with a merge function
Map<String, Integer> nameToAge = students.stream()
    .collect(Collectors.toMap(
        Student::getName,
        Student::getAge,
        (existing, newVal) -> existing  // keep existing on collision
    ));

groupingBy — group elements by a classifier

// Group students by department
Map<String, List<Student>> byDept = students.stream()
    .collect(Collectors.groupingBy(Student::getDepartment));

// Group and count
Map<String, Long> countByDept = students.stream()
    .collect(Collectors.groupingBy(
        Student::getDepartment,
        Collectors.counting()
    ));

// Group and collect only names
Map<String, List<String>> namesByDept = students.stream()
    .collect(Collectors.groupingBy(
        Student::getDepartment,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));

partitioningBy — split into two groups (true/false)

Map<Boolean, List<Student>> partition = students.stream()
    .collect(Collectors.partitioningBy(s -> s.getAge() >= 18));

List<Student> adults = partition.get(true);
List<Student> minors = partition.get(false);

joining — concatenate strings

String result = names.stream().collect(Collectors.joining());
// "AliceBobCharlie"

String result = names.stream().collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"

String result = names.stream().collect(Collectors.joining(", ", "[", "]"));
// "[Alice, Bob, Charlie]"

counting, summingInt, averagingInt

long count = stream.collect(Collectors.counting());
int total  = stream.collect(Collectors.summingInt(Student::getAge));
double avg = stream.collect(Collectors.averagingInt(Student::getAge));

Primitive streams

For int, long, and double, use IntStream, LongStream, DoubleStream to avoid boxing overhead. They have extra methods like sum(), average(), range().

// Sum of 1 to 100
int sum = IntStream.rangeClosed(1, 100).sum();

// Average age
OptionalDouble avg = students.stream()
    .mapToInt(Student::getAge)
    .average();

// Box back to Stream<Integer> if needed
Stream<Integer> boxed = IntStream.range(0, 10).boxed();

Common patterns

Filter then transform:

List<String> result = students.stream()
    .filter(s -> s.getAge() > 20)
    .map(Student::getName)
    .sorted()
    .collect(Collectors.toList());

Frequency map (word count):

Map<String, Long> freq = words.stream()
    .collect(Collectors.groupingBy(w -> w, Collectors.counting()));

Flatten and deduplicate:

Set<String> allTags = posts.stream()
    .flatMap(p -> p.getTags().stream())
    .collect(Collectors.toSet());

Top N elements:

List<Student> top3 = students.stream()
    .sorted(Comparator.comparingInt(Student::getScore).reversed())
    .limit(3)
    .collect(Collectors.toList());

Check if a value exists and get it:

students.stream()
    .filter(s -> s.getName().equals("Alice"))
    .findFirst()
    .ifPresent(s -> System.out.println(s.getAge()));

Parallel streams

Call .parallelStream() instead of .stream() to split work across threads. Useful for CPU-bound operations on large collections.

long count = bigList.parallelStream()
    .filter(expensiveOperation)
    .count();

Caveats:

  • Not always faster — parallelism has overhead. Benchmark before using.
  • Order is not guaranteed unless you use forEachOrdered or sorted.
  • Avoid stateful lambdas (e.g., modifying a shared variable) — they cause race conditions.

Quick reference

Operation Type What it does
filter(pred) Intermediate Keep matching elements
map(fn) Intermediate Transform each element
flatMap(fn) Intermediate Transform and flatten
sorted(cmp) Intermediate Sort elements
distinct() Intermediate Remove duplicates
limit(n) Intermediate Take first n
skip(n) Intermediate Skip first n
peek(fn) Intermediate Inspect without consuming
collect(c) Terminal Gather into collection
forEach(fn) Terminal Consume each element
count() Terminal Count elements
findFirst() Terminal First element as Optional
anyMatch(pred) Terminal Short-circuit OR
allMatch(pred) Terminal Short-circuit AND
reduce(id, fn) Terminal Fold into single value
min(cmp) / max(cmp) Terminal Minimum / maximum as Optional

Tags: java, streams, functional-programming