The Stream API (since Java 8) provides a declarative way to process sequences of elements — filtering, mapping, reducing, and collecting — often in a single pipeline.

Creating Streams

  // From a Collection
List<String> list = List.of("a", "b", "c");
Stream<String> stream = list.stream();

// From an array
Stream<Integer> intStream = Arrays.stream(new int[]{1, 2, 3});

// Static factory methods
Stream<String> of = Stream.of("x", "y", "z");
Stream<String> empty = Stream.empty();
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1).limit(10);
Stream<Integer> generated = Stream.generate(() -> (int)(Math.random() * 100)).limit(5);
  

Intermediate Operations

Return a new stream; are lazy until a terminal operation runs.

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

List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))   // Alice, Anna
    .map(String::toUpperCase)                  // ALICE, ANNA
    .sorted()                                  // ALICE, ANNA
    .toList();                                 // terminal (Java 16+)
  

Common intermediate operations:

Operation Description
filter(Predicate) Keep elements matching condition
map(Function) Transform each element
flatMap(Function) Flatten nested streams
distinct() Remove duplicates
sorted() Sort elements
peek(Consumer) Observe elements (debugging)
limit(n) Truncate to n elements
skip(n) Skip first n elements

flatMap Example

  List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .toList(); // [1, 2, 3, 4]
  

Terminal Operations

Trigger processing and produce a result or side effect.

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

// Collect to list
List<Integer> evens = numbers.stream()
    .filter(n -> n % 2 == 0)
    .toList(); // [2, 4]

// Reduce
Optional<Integer> sum = numbers.stream()
    .reduce((a, b) -> a + b); // 15

// Count
long count = numbers.stream().filter(n -> n > 2).count(); // 3

// Match
boolean allPositive = numbers.stream().allMatch(n -> n > 0); // true

// Find
Optional<Integer> first = numbers.stream()
    .filter(n -> n > 3)
    .findFirst(); // 4

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

Collectors

  import java.util.stream.Collectors;

List<String> words = List.of("apple", "banana", "cherry", "apricot");

// Group by first letter
Map<Character, List<String>> grouped = words.stream()
    .collect(Collectors.groupingBy(w -> w.charAt(0)));

// Join strings
String joined = words.stream()
    .collect(Collectors.joining(", ")); // apple, banana, cherry, apricot

// Statistics
IntSummaryStatistics stats = words.stream()
    .collect(Collectors.summarizingInt(String::length));
System.out.println(stats.getAverage()); // average word length

// Partition
Map<Boolean, List<String>> partition = words.stream()
    .collect(Collectors.partitioningBy(w -> w.length() > 5));
  

Parallel Streams

  List<Integer> numbers = IntStream.range(0, 1_000_000)
    .boxed()
    .toList();

long sum = numbers.parallelStream()
    .mapToLong(Integer::longValue)
    .sum();
  

Use parallel streams when:

  • Data set is large
  • Operations are CPU-intensive and side-effect free
  • Source supports efficient splitting (ArrayList, arrays)

Avoid parallel streams for small data sets, I/O-bound tasks, or when order matters.

Best Practices

  • Prefer streams for declarative data processing; use loops when logic is complex
  • Avoid modifying external state inside stream operations
  • Use toList() (Java 16+) instead of collect(Collectors.toList())
  • Do not overuse parallel streams — measure before optimizing
  • Close streams backed by I/O resources (e.g., Files.lines())