Java · Functional Style · Data Processing

Java Streams API

The Streams API provides a declarative way to process sequences of data using pipelines of operations such as filtering, mapping, sorting, reducing, and collecting.

What is a Stream?

A stream is a sequence of elements processed through a pipeline. A stream does not store data itself; it reads from a source such as a collection, array, file, or generated sequence.

Important: a stream is not a data structure. It is a processing abstraction over a data source.

Stream Pipeline

A typical pipeline has three parts:

  • Source — for example a List.
  • Intermediate operations — such as filter, map, and sorted.
  • Terminal operation — such as collect, forEach, reduce, or count.
List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .sorted()
    .toList();

Intermediate Operations

OperationPurpose
filterKeeps elements that satisfy a predicate.
mapTransforms each element into another value.
flatMapFlattens nested streams into one stream.
sortedProduces elements in sorted order.
distinctRemoves duplicates according to equals.
limitKeeps only the first N elements.
skipSkips the first N elements.
peekObserves elements, usually for debugging.

Terminal Operations

OperationResult
toList / collectBuilds a result collection.
forEachPerforms an action for each element.
countReturns the number of elements.
reduceCombines elements into one result.
findFirstReturns the first element as an Optional.
anyMatchChecks whether any element matches a predicate.
allMatchChecks whether all elements match a predicate.
noneMatchChecks whether no elements match a predicate.

Lazy Evaluation

Most intermediate operations are lazy. They describe work but do not process the source until a terminal operation starts the pipeline.

Stream<String> pipeline = names.stream()
    .filter(name -> {
        System.out.println("checking " + name);
        return name.length() > 3;
    });

// Nothing is processed yet.

long count = pipeline.count();

Streams Are Single-Use

After a terminal operation, the stream is consumed and cannot be reused.

Stream<String> stream = names.stream();

long count = stream.count();

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

Mapping and Filtering Example

List<String> names =
    List.of("Alice", "Bob", "Charlie", "Anna");

List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .toList();

System.out.println(result);
// [ALICE, ANNA]

Reducing

reduce combines many elements into one value.

int sum = List.of(1, 2, 3, 4, 5)
    .stream()
    .reduce(0, Integer::sum);

Collectors

Collectors provide reusable reduction strategies for grouping, joining, counting, averaging, and building collections.

Map<Integer, List<String>> byLength =
    names.stream()
         .collect(Collectors.groupingBy(String::length));
String joined = names.stream()
    .collect(Collectors.joining(", "));

Primitive Streams

Java provides IntStream, LongStream, and DoubleStream to avoid unnecessary boxing when processing primitive numeric values.

int total = IntStream.rangeClosed(1, 100)
    .sum();

Optional Results

Optional<String> first =
    names.stream()
         .filter(name -> name.startsWith("Z"))
         .findFirst();

first.ifPresent(System.out::println);

Side Effects

Stream operations are easiest to understand when lambdas avoid modifying shared external state.

// Better:
List<String> upper =
    names.stream()
         .map(String::toUpperCase)
         .toList();

Using external mutable collections inside forEach can make code harder to reason about and becomes especially risky with parallel streams.

Parallel Streams

long count = numbers.parallelStream()
    .filter(n -> expensiveCheck(n))
    .count();

Parallel streams divide work across threads, but they are not automatically faster. Performance depends on workload size, operation cost, data structure, ordering constraints, available processors, and thread-pool contention.

Use parallel streams only after measuring. Small pipelines, blocking I/O, shared mutable state, and order-sensitive operations may perform worse or behave unpredictably.

Common Mistakes

  • Trying to reuse a stream after a terminal operation.
  • Using peek as the main business operation instead of for observation/debugging.
  • Adding side effects to operations that should behave like transformations.
  • Assuming parallel streams always improve performance.
  • Forgetting that operations such as sorted may require buffering many elements.

Conclusion

Streams are best understood as lazy, single-use processing pipelines. Learn the difference between sources, intermediate operations, and terminal operations, then add collectors, reductions, primitive streams, and carefully measured parallelism.