Java Streams API

The Java Streams API (introduced in Java 8) provides a declarative, functional-style way to process collections of data.
It allows you to perform complex data transformations without explicit loops, making code more readable, concise, and expressive.


What is a Stream?

A Stream is:

  • Not a data structure
  • A pipeline of operations
  • Used to process data from a source (Collection, Array, I/O, etc.)

A stream does not store data.
It processes data on demand.


Key Characteristics

  • ❌ No data storage
  • ✅ Lazy evaluation
  • ❌ Cannot be reused
  • ✅ Functional-style operations
  • ✅ Can be sequential or parallel

Stream Pipeline

A stream pipeline has three parts:

Source → Intermediate Operations → Terminal Operation

list.stream()
    .filter(n -> n > 10)
    .map(n -> n * 2)
    .forEach(System.out::println);

Stream Sources

Streams can be created from:

Collections

list.stream();
list.parallelStream();

Arrays

Arrays.stream(arr);

Static Stream methods

Stream.of(1, 2, 3);
IntStream.range(1, 5);

Strings

s.chars();       // IntStream
s.codePoints();  // Unicode-safe

Types of Streams

Object Stream

Stream<T>

Primitive Streams

  • IntStream
  • LongStream
  • DoubleStream

Primitive streams exist to avoid boxing/unboxing overhead.


Intermediate Operations

Intermediate operations:

  • Return a new stream
  • Are lazy
  • Do not execute until a terminal operation is called

Common Intermediate Operations

filter

stream.filter(x -> x > 5);

map

stream.map(x -> x * 2);

distinct

stream.distinct();

sorted

stream.sorted();

limit / skip

stream.limit(5);
stream.skip(3);

map vs mapToInt / mapToObj

stream.map(x -> x.toString());

Primitive streams need conversion:

IntStream.mapToObj(i -> String.valueOf(i));

mapToObj() exists only on primitive streams.


Terminal Operations

Terminal operations:

  • Trigger execution
  • Consume the stream
  • Produce a result or side effect

Common Terminal Operations

forEach

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

collect

List<Integer> list = stream.collect(Collectors.toList());

reduce

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

count

long count = stream.count();

findFirst / findAny

stream.findFirst();

anyMatch / allMatch / noneMatch

stream.anyMatch(x -> x > 10);

Collectors

Collectors are utility methods for collecting stream results.

Common Collectors

Collectors.toList()
Collectors.toSet()
Collectors.toMap()
Collectors.joining(", ")
Collectors.groupingBy()
Collectors.partitioningBy()

Example:

Map<String, List<Employee>> byDept =
    employees.stream()
             .collect(Collectors.groupingBy(Employee::getDepartment));

Lazy Evaluation

Streams execute only when needed.

stream.filter(x -> {
    System.out.println(x);
    return x > 5;
});

☝️ Nothing prints until a terminal operation is called.


Parallel Streams

Parallel streams split work across threads automatically.

list.parallelStream()
    .map(this::process)
    .collect(Collectors.toList());

When to use

  • Large datasets
  • CPU-bound tasks
  • Stateless operations

When NOT to use

  • I/O operations
  • Small datasets
  • Order-sensitive logic

Stream vs Collection

Feature Collection Stream
Stores data Yes No
Can iterate multiple times Yes No
Lazy No Yes
Functional style No Yes

Common Pitfalls

  • Reusing a stream ❌
stream.forEach(...);
stream.count(); // IllegalStateException
  • Modifying source during stream execution ❌
  • Using streams for simple loops unnecessarily ❌

When to Use Streams

✅ Data transformation ✅ Filtering, mapping, grouping ✅ Readability over control

❌ Complex control flow ❌ Heavy mutation logic


Summary

  • Streams provide a clean, functional approach to data processing
  • Separate what you want from how it’s done
  • Lazy, expressive, and powerful
  • Best used for transformation-heavy logic

One-line takeaway

Streams turn loops into readable data pipelines.


This site uses Just the Docs, a documentation theme for Jekyll.