The Streams API (Java 8+) processes collections declaratively: say what you want (filter, transform, group) instead of writing loops. In automation it turns lists of WebElements and API responses into exactly the data you assert on, for example elements.stream().map(WebElement::getText).toList().

Streams API

In Simple Terms

Stream = Assembly line for data:

Imagine a conveyor belt. Raw materials (data) go in one end. Workers along the belt filter bad ones (filter), change them (map), count them (count), or box them (collect). Streams are your data assembly line in Java.

Stream Pipeline

import java.util.stream.*;

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Stream pipeline: source → intermediate ops → terminal op
List<Integer> result = numbers.stream()  // source
    .filter(n -> n % 2 == 0)             // keep evens: [2,4,6,8,10]
    .map(n -> n * n)                     // square: [4,16,36,64,100]
    .filter(n -> n > 20)                 // keep > 20: [36,64,100]
    .sorted()                            // sort: [36,64,100]
    .collect(Collectors.toList());       // terminal: collect

System.out.println(result);  // [36, 64, 100]

All Stream Operations

List<String> words = Arrays.asList("hello","world","java","stream","api");

// ── INTERMEDIATE OPERATIONS (lazy — don't run until terminal) ──
words.stream().filter(s -> s.length() > 4);    // keep: world,stream
words.stream().map(String::toUpperCase);         // transform each
words.stream().flatMap(s -> Arrays.stream(s.split(""))); // flatten
words.stream().distinct();                       // remove duplicates
words.stream().sorted();                         // natural sort
words.stream().sorted(Comparator.reverseOrder()); // custom sort
words.stream().limit(3);                         // first 3
words.stream().skip(2);                          // skip first 2
words.stream().peek(System.out::println);         // debug/side-effect

// ── TERMINAL OPERATIONS (trigger execution) ──
long count = words.stream().count();                         // 5
Optional<String> first = words.stream().findFirst();         // Optional[hello]
Optional<String> any = words.stream().findAny();             // any one
boolean allLong = words.stream().allMatch(s -> s.length() > 2); // true
boolean anyLong = words.stream().anyMatch(s -> s.length() > 5); // true (stream)
boolean noneLong = words.stream().noneMatch(s -> s.length() > 10); // true
Optional<String> min = words.stream().min(Comparator.naturalOrder()); // api
Optional<String> max = words.stream().max(Comparator.naturalOrder()); // world

// reduce: combine all elements
Optional<String> concat = words.stream().reduce((a, b) -> a + " " + b);
String concat2 = words.stream().reduce("", (a, b) -> a + b);

// forEach: consume each
words.stream().forEach(System.out::println);

// toArray
Object[] arr = words.stream().toArray();
String[] sarr = words.stream().toArray(String[]::new);

Collectors — Powerful Collecting

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

// Collect to collections
List<String> list = names.stream().collect(Collectors.toList());
Set<String> set = names.stream().collect(Collectors.toSet());
LinkedList<String> ll = names.stream().collect(
    Collectors.toCollection(LinkedList::new));

// Join strings
String joined = names.stream().collect(Collectors.joining(", ", "[", "]"));
// [Alice, Bob, Charlie, Anna, Brian]

// Grouping
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));
// {5=[Alice, Brian], 3=[Bob, Anna], 7=[Charlie]}

Map<Integer, Long> countByLength = names.stream()
    .collect(Collectors.groupingBy(String::length, Collectors.counting()));

// Partitioning (2 groups: true/false)
Map<Boolean, List<String>> partition = names.stream()
    .collect(Collectors.partitioningBy(s -> s.startsWith("A")));
// {false=[Bob, Charlie, Brian], true=[Alice, Anna]}

// Statistics
IntSummaryStatistics stats = names.stream()
    .collect(Collectors.summarizingInt(String::length));
// count=5, sum=23, min=3, max=7, avg=4.6

// toMap
Map<String, Integer> nameToLength = names.stream()
    .collect(Collectors.toMap(
        name -> name,
        String::length,
        (e1, e2) -> e1  // merge function for duplicate keys
    ));

Parallel Streams

// Parallel stream: splits work across multiple threads (ForkJoinPool)
long count = numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .count();

// Convert existing stream to parallel
numbers.stream().parallel().forEach(System.out::println);

// CAUTION with parallel streams:
// 1. Order not guaranteed (use forEachOrdered if needed)
// 2. Stateful ops (sorted, distinct) expensive in parallel
// 3. Not always faster — overhead for small datasets
// 4. NOT thread-safe for mutable state

// BAD: shared mutable state in parallel
List<Integer> shared = new ArrayList<>();
numbers.parallelStream().forEach(shared::add);  // RACE CONDITION!

// GOOD: use thread-safe collector
List<Integer> safe = numbers.parallelStream()
    .collect(Collectors.toList());  // thread-safe

Stream Lazy Evaluation

// Intermediate operations are LAZY — not executed until terminal op
Stream<Integer> stream = List.of(1, 2, 3, 4, 5).stream()
    .filter(n -> { System.out.println("filter: " + n); return n > 2; })
    .map(n -> { System.out.println("map: " + n); return n * 2; });

// Nothing printed yet! Stream not executed.

stream.findFirst();  // Terminal op → now it executes!
// Output: filter: 1, filter: 2, filter: 3, map: 3
// Short-circuited! Stopped after finding first match

// Key insight: stream processes elements one at a time
// through the entire pipeline (not stage-by-stage batch)

Interview Questions

What is the difference between map() and flatMap()?

map() transforms each element to one output (1:1). flatMap() transforms each element to a Stream and flattens all streams into one (1:many). Example: flatMap on List<List<String>> flattens to Stream<String>. Use flatMap when transformation returns a collection.

Can a stream be reused?

No! A stream can only be consumed ONCE. After a terminal operation, the stream is closed. Attempting to use it again throws IllegalStateException. Create a new stream from the source for each operation chain.

When should you NOT use parallel streams?

When dataset is small, when operations are stateful (depend on order or shared state), when operations have side effects, when sequential overhead is low. Parallel streams shine with large datasets and CPU-intensive, independent operations.

Advertisement

Primitive Streams — IntStream, LongStream, DoubleStream

// Primitive streams avoid boxing/unboxing overhead

// ── CREATE ──
IntStream.of(1, 2, 3, 4, 5)
IntStream.range(1, 6)         // [1,2,3,4,5] (exclusive end)
IntStream.rangeClosed(1, 5)   // [1,2,3,4,5] (inclusive end)
IntStream.iterate(0, n -> n+2).limit(5) // 0,2,4,6,8
IntStream.generate(() -> 1).limit(3)    // 1,1,1
"Hello".chars()               // IntStream of char values
new Random().ints(5, 0, 100)  // 5 random ints 0-99

// ── OPERATIONS ──
IntStream stream = IntStream.rangeClosed(1, 10);
stream.sum()          // 55
stream.average()      // OptionalDouble(5.5)
stream.min()          // OptionalInt(1)
stream.max()          // OptionalInt(10)
stream.count()        // 10
stream.summaryStatistics() // IntSummaryStatistics{count,sum,min,max,avg}

// Filter, map, reduce
int sumOfSquares = IntStream.rangeClosed(1, 5)
    .filter(n -> n % 2 != 0)  // 1,3,5
    .map(n -> n * n)           // 1,9,25
    .sum();                     // 35

// ── CONVERT ──
// IntStream → Stream<Integer> (boxing)
Stream<Integer> boxed = IntStream.range(1,5).boxed();
Stream<String> asString = IntStream.range(1,5).mapToObj(String::valueOf);

// Stream<Integer> → IntStream (unboxing)
IntStream unboxed = Stream.of(1,2,3).mapToInt(Integer::intValue);

// IntStream → LongStream
LongStream asLong = IntStream.range(1,5).asLongStream();

// ── PRACTICAL EXAMPLES ──
// Sum of digits
int digitSum = String.valueOf(12345).chars()
    .map(c -> c - '0')
    .sum(); // 15

// Character frequency
Map<Integer, Long> freq = "hello world".chars()
    .boxed()
    .collect(Collectors.groupingBy(c->c, Collectors.counting()));

// Generate multiplication table
IntStream.rangeClosed(1, 10)
    .forEach(i -> {
        System.out.println(IntStream.rangeClosed(1, 10)
            .mapToObj(j -> String.format("%4d", i*j))
            .collect(Collectors.joining()));
    });