Streams

1. What is the Stream API?

Think of a conveyor belt in a sushi restaurant. Plates of sushi travel along the belt, and you — the customer — pick only the ones you want, maybe add some soy sauce, and eat them. You never rearrange the kitchen. You never touch the plates you skip. You simply describe what you want and let the system deliver it.

The Java Stream API, introduced in Java 8 as part of the java.util.stream package, brings this declarative processing model to collections and other data sources. Instead of writing explicit loops that tell the computer how to iterate, you write a pipeline that describes what transformations and filters to apply.

Key characteristics of Streams:

  • Not a data structure — A Stream does not store elements. It reads elements from a source (Collection, array, file, generator) and pushes them through a pipeline of operations.
  • Declarative pipeline — You chain operations like filter, map, and reduce to express complex data processing in a readable, composable way.
  • Lazy evaluation — Intermediate operations (filter, map, sorted) are not executed until a terminal operation (collect, forEach, count) triggers the pipeline. This allows optimizations like short-circuiting.
  • Does not modify the source — Streaming over a List does not add, remove, or change elements in that List. The original data remains untouched.
  • Single use — Once a terminal operation is invoked, the Stream is consumed. You must create a new Stream to process the same data again.
  • Parallelism built in — Switch from sequential to parallel processing with a single method call (parallelStream()), leveraging multiple CPU cores automatically.

Stream Pipeline Structure

Every Stream pipeline has exactly three parts:

Part Description Examples
Source Where the data originates list.stream(), Arrays.stream(arr), Stream.of()
Intermediate Operations Transform the stream (lazy, return a new Stream) filter(), map(), sorted(), distinct()
Terminal Operation Produces a result or side-effect (triggers execution) collect(), forEach(), count(), reduce()

Here is a visual representation of how a Stream pipeline works:

Source          Intermediate Ops         Terminal Op
[data] ----> filter() ----> map() ----> collect()
                 |              |              |
            (lazy)         (lazy)      (triggers all)
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamIntro {
    public static void main(String[] args) {
        List names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

        // Stream pipeline: source -> intermediate ops -> terminal op
        List result = names.stream()          // Source: create stream from list
            .filter(n -> n.length() > 3)              // Intermediate: keep names longer than 3 chars
            .map(String::toUpperCase)                  // Intermediate: convert to uppercase
            .sorted()                                  // Intermediate: sort alphabetically
            .collect(Collectors.toList());             // Terminal: collect into a new list

        System.out.println("Result: " + result);
        // Output: Result: [ALICE, CHARLIE, DAVID]

        // The original list is unchanged
        System.out.println("Original: " + names);
        // Output: Original: [Alice, Bob, Charlie, David, Eve]

        // Streams are single-use -- this will throw IllegalStateException
        var stream = names.stream();
        stream.count(); // terminal operation consumes the stream
        // stream.forEach(System.out::println); // ERROR: stream has already been operated upon
    }
}

2. Creating Streams

Before you can process data with the Stream API, you need to create a Stream. Java provides multiple ways depending on the data source.

2.1 From Collections

The most common way. Every class implementing Collection (List, Set, Queue) inherits a stream() method.

import java.util.*;
import java.util.stream.Collectors;

public class StreamFromCollections {
    public static void main(String[] args) {
        // From a List
        List languages = List.of("Java", "Python", "Go", "Rust");
        List filtered = languages.stream()
            .filter(lang -> lang.length() > 2)
            .collect(Collectors.toList());
        System.out.println("Filtered: " + filtered);
        // Output: Filtered: [Java, Python, Rust]

        // From a Set
        Set numbers = new LinkedHashSet<>(Set.of(5, 3, 1, 4, 2));
        int sum = numbers.stream()
            .filter(n -> n % 2 != 0)
            .mapToInt(Integer::intValue)
            .sum();
        System.out.println("Sum of odds: " + sum);

        // From a Map (via entrySet, keySet, or values)
        Map scores = Map.of("Alice", 92, "Bob", 78, "Charlie", 95);
        scores.entrySet().stream()
            .filter(e -> e.getValue() >= 90)
            .forEach(e -> System.out.println(e.getKey() + " scored " + e.getValue()));
        // Output: Alice scored 92
        //         Charlie scored 95
    }
}

2.2 From Arrays

Use Arrays.stream() for arrays. For primitive arrays (int[], long[], double[]), it returns the corresponding primitive stream (IntStream, LongStream, DoubleStream).

import java.util.Arrays;
import java.util.stream.Stream;

public class StreamFromArrays {
    public static void main(String[] args) {
        String[] colors = {"Red", "Green", "Blue", "Yellow"};

        // Full array stream
        Arrays.stream(colors).forEach(System.out::println);

        // Partial array: from index 1 (inclusive) to 3 (exclusive)
        String[] partial = Arrays.stream(colors, 1, 3)
            .toArray(String[]::new);
        System.out.println(Arrays.toString(partial));
        // Output: [Green, Blue]

        // Stream.of() works with varargs
        Stream.of("One", "Two", "Three").forEach(System.out::println);

        // Primitive array -- returns IntStream, not Stream
        int[] numbers = {10, 20, 30, 40};
        int sum = Arrays.stream(numbers).sum();
        System.out.println("Sum: " + sum); // Output: Sum: 100

        double avg = Arrays.stream(numbers).average().orElse(0.0);
        System.out.println("Average: " + avg); // Output: Average: 25.0
    }
}

2.3 Stream.of(), generate(), and iterate()

Java provides static factory methods on the Stream interface to create streams without needing an existing collection or array.

import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamFactoryMethods {
    public static void main(String[] args) {
        // Stream.empty() -- useful as a return value instead of null
        Stream empty = Stream.empty();
        System.out.println("Empty count: " + empty.count()); // Output: 0

        // Stream.of() -- create from individual elements
        Stream.of("Java", "Python", "Go")
            .map(String::toUpperCase)
            .forEach(System.out::println);

        // Stream.generate() -- infinite stream from a Supplier
        // ALWAYS use limit() or the stream runs forever
        List uuids = Stream.generate(UUID::randomUUID)
            .limit(3)
            .map(UUID::toString)
            .collect(Collectors.toList());
        System.out.println("UUIDs: " + uuids);

        // Stream.iterate() -- Java 8 style (seed + UnaryOperator, must limit)
        List powersOf2 = Stream.iterate(1, n -> n * 2)
            .limit(8)
            .collect(Collectors.toList());
        System.out.println("Powers: " + powersOf2);
        // Output: Powers: [1, 2, 4, 8, 16, 32, 64, 128]

        // Stream.iterate() -- Java 9+ style (seed + predicate + UnaryOperator)
        Stream.iterate(1, n -> n <= 100, n -> n * 2)
            .forEach(n -> System.out.print(n + " "));
        // Output: 1 2 4 8 16 32 64
        System.out.println();

        // Stream.concat() -- combine two streams into one
        Stream s1 = Stream.of("A", "B");
        Stream s2 = Stream.of("C", "D");
        Stream.concat(s1, s2).forEach(System.out::println);
        // Output: A, B, C, D
    }
}

2.4 IntStream.range() and rangeClosed()

For generating sequences of integers, IntStream provides range() (exclusive end) and rangeClosed() (inclusive end). These are more efficient than Stream.iterate() for simple numeric ranges.

import java.util.stream.IntStream;
import java.util.stream.LongStream;

public class StreamRanges {
    public static void main(String[] args) {
        // range(start, endExclusive) -- does NOT include the end value
        IntStream.range(1, 5).forEach(n -> System.out.print(n + " "));
        // Output: 1 2 3 4
        System.out.println();

        // rangeClosed(start, endInclusive) -- INCLUDES the end value
        IntStream.rangeClosed(1, 5).forEach(n -> System.out.print(n + " "));
        // Output: 1 2 3 4 5
        System.out.println();

        // Sum of 1 to 100
        int sum = IntStream.rangeClosed(1, 100).sum();
        System.out.println("Sum 1-100: " + sum); // Output: Sum 1-100: 5050

        // LongStream for larger ranges
        long bigSum = LongStream.rangeClosed(1, 1_000_000).sum();
        System.out.println("Sum 1-1M: " + bigSum);
        // Output: Sum 1-1M: 500000500000

        // Using range as a loop replacement
        IntStream.range(0, 5)
            .mapToObj(i -> "Item " + i)
            .forEach(System.out::println);
    }
}

Stream Creation Summary

Method Returns Use Case
collection.stream() Stream<T> Most common — stream from any Collection
Arrays.stream(array) Stream<T> or primitive stream Stream from an array
Stream.of(a, b, c) Stream<T> Stream from individual values
Stream.empty() Stream<T> Empty stream (null-safe return)
Stream.generate(supplier) Stream<T> Infinite stream from a Supplier
Stream.iterate(seed, op) Stream<T> Infinite stream with iteration
Stream.concat(s1, s2) Stream<T> Merge two streams
IntStream.range(a, b) IntStream Range of ints [a, b)
IntStream.rangeClosed(a, b) IntStream Range of ints [a, b]

3. Intermediate Operations

Intermediate operations transform a Stream into another Stream. They are lazy — nothing happens until a terminal operation triggers the pipeline. You can chain as many intermediate operations as you need. Each one returns a new Stream, leaving the original Stream unchanged.

3.1 filter(Predicate)

filter() keeps only the elements that match the given condition. Elements that pass the test continue down the pipeline; those that fail are discarded.

import java.util.List;
import java.util.stream.Collectors;

public class FilterExample {
    public static void main(String[] args) {
        List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Keep only even numbers
        List evens = numbers.stream()
            .filter(n -> n % 2 == 0)
            .collect(Collectors.toList());
        System.out.println("Evens: " + evens);
        // Output: Evens: [2, 4, 6, 8, 10]

        // Chaining multiple filters (equivalent to AND)
        List result = numbers.stream()
            .filter(n -> n > 3)
            .filter(n -> n < 8)
            .collect(Collectors.toList());
        System.out.println("Between 3 and 8: " + result);
        // Output: Between 3 and 8: [4, 5, 6, 7]

        // Filtering with complex predicates
        List names = List.of("Alice", "Bob", "Charlie", "Anna", "Andrew");
        List startsWithA = names.stream()
            .filter(name -> name.startsWith("A"))
            .filter(name -> name.length() > 4)
            .collect(Collectors.toList());
        System.out.println("Starts with A, length > 4: " + startsWithA);
        // Output: Starts with A, length > 4: [Alice, Andrew]
    }
}

3.2 map(Function)

map() transforms each element by applying a function. The input type and output type can be different. This is one of the most commonly used operations.

import java.util.List;
import java.util.stream.Collectors;

public class MapExample {
    public static void main(String[] args) {
        // Transform strings to uppercase
        List names = List.of("alice", "bob", "charlie");
        List upper = names.stream()
            .map(String::toUpperCase)
            .collect(Collectors.toList());
        System.out.println("Upper: " + upper);
        // Output: Upper: [ALICE, BOB, CHARLIE]

        // Transform to different type: String -> Integer (length)
        List lengths = names.stream()
            .map(String::length)
            .collect(Collectors.toList());
        System.out.println("Lengths: " + lengths);
        // Output: Lengths: [5, 3, 7]

        // Chain map operations
        List formatted = names.stream()
            .map(String::trim)
            .map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
            .map(s -> "Hello, " + s + "!")
            .collect(Collectors.toList());
        System.out.println("Formatted: " + formatted);
        // Output: Formatted: [Hello, Alice!, Hello, Bob!, Hello, Charlie!]

        // Extract a field from objects
        List emails = List.of("alice@mail.com", "bob@mail.com", "charlie@mail.com");
        List usernames = emails.stream()
            .map(email -> email.split("@")[0])
            .collect(Collectors.toList());
        System.out.println("Usernames: " + usernames);
        // Output: Usernames: [alice, bob, charlie]
    }
}

3.3 flatMap(Function)

flatMap() is used when each element maps to multiple elements (a stream of streams). It flattens the nested streams into a single stream. This is essential when dealing with lists of lists, optional values, or one-to-many transformations.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapExample {
    public static void main(String[] args) {
        // Problem: List of lists -- map gives Stream>
        List> nested = List.of(
            List.of("Java", "Python"),
            List.of("Go", "Rust"),
            List.of("JavaScript", "TypeScript")
        );

        // map() would give us Stream> -- not what we want
        // flatMap() flattens it into Stream
        List allLanguages = nested.stream()
            .flatMap(List::stream)
            .collect(Collectors.toList());
        System.out.println("All: " + allLanguages);
        // Output: All: [Java, Python, Go, Rust, JavaScript, TypeScript]

        // Split sentences into words
        List sentences = List.of(
            "Java is great",
            "Streams are powerful",
            "FlatMap flattens"
        );
        List words = sentences.stream()
            .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
            .map(String::toLowerCase)
            .distinct()
            .sorted()
            .collect(Collectors.toList());
        System.out.println("Words: " + words);
        // Output: Words: [are, flatmap, flattens, great, is, java, powerful, streams]

        // flatMap with arrays
        String[][] data = {{"a", "b"}, {"c", "d"}, {"e", "f"}};
        List flat = Arrays.stream(data)
            .flatMap(Arrays::stream)
            .collect(Collectors.toList());
        System.out.println("Flat: " + flat);
        // Output: Flat: [a, b, c, d, e, f]
    }
}

3.4 sorted()

sorted() sorts the elements. Without arguments, it uses natural ordering (elements must implement Comparable). With a Comparator argument, you control the sort order.

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class SortedExample {
    public static void main(String[] args) {
        List names = List.of("Charlie", "Alice", "Eve", "Bob", "David");

        // Natural ordering (alphabetical for Strings)
        List sorted = names.stream()
            .sorted()
            .collect(Collectors.toList());
        System.out.println("Sorted: " + sorted);
        // Output: Sorted: [Alice, Bob, Charlie, David, Eve]

        // Reverse order
        List reversed = names.stream()
            .sorted(Comparator.reverseOrder())
            .collect(Collectors.toList());
        System.out.println("Reversed: " + reversed);
        // Output: Reversed: [Eve, David, Charlie, Bob, Alice]

        // Sort by length
        List byLength = names.stream()
            .sorted(Comparator.comparingInt(String::length))
            .collect(Collectors.toList());
        System.out.println("By length: " + byLength);
        // Output: By length: [Bob, Eve, Alice, David, Charlie]

        // Sort by length descending, then alphabetically
        List complex = names.stream()
            .sorted(Comparator.comparingInt(String::length)
                .reversed()
                .thenComparing(Comparator.naturalOrder()))
            .collect(Collectors.toList());
        System.out.println("Complex sort: " + complex);
        // Output: Complex sort: [Charlie, Alice, David, Bob, Eve]
    }
}

3.5 distinct(), peek(), limit(), and skip()

These operations help you control which elements pass through the pipeline and inspect elements during processing.

Operation Description
distinct() Removes duplicates (uses equals())
peek(Consumer) Performs an action on each element without consuming it — useful for debugging
limit(n) Truncates the stream to at most n elements
skip(n) Discards the first n elements
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MoreIntermediateOps {
    public static void main(String[] args) {
        // distinct() -- removes duplicates
        List numbers = List.of(1, 2, 3, 2, 1, 4, 3, 5);
        List unique = numbers.stream()
            .distinct()
            .collect(Collectors.toList());
        System.out.println("Unique: " + unique);
        // Output: Unique: [1, 2, 3, 4, 5]

        // peek() -- inspect elements without consuming (great for debugging)
        List result = List.of("one", "two", "three", "four").stream()
            .filter(s -> s.length() > 3)
            .peek(s -> System.out.println("After filter: " + s))
            .map(String::toUpperCase)
            .peek(s -> System.out.println("After map: " + s))
            .collect(Collectors.toList());
        // Output:
        // After filter: three
        // After map: THREE
        // After filter: four
        // After map: FOUR
        System.out.println("Result: " + result);
        // Output: Result: [THREE, FOUR]

        // limit(n) -- take only the first n elements
        List firstThree = List.of(10, 20, 30, 40, 50).stream()
            .limit(3)
            .collect(Collectors.toList());
        System.out.println("First 3: " + firstThree);
        // Output: First 3: [10, 20, 30]

        // skip(n) -- skip the first n elements
        List skipTwo = List.of(10, 20, 30, 40, 50).stream()
            .skip(2)
            .collect(Collectors.toList());
        System.out.println("Skip 2: " + skipTwo);
        // Output: Skip 2: [30, 40, 50]

        // Pagination: skip + limit
        List allItems = List.of("A", "B", "C", "D", "E", "F", "G", "H");
        int pageSize = 3;
        int pageNumber = 2; // 0-based
        List page = allItems.stream()
            .skip((long) pageNumber * pageSize)
            .limit(pageSize)
            .collect(Collectors.toList());
        System.out.println("Page 2: " + page);
        // Output: Page 2: [G, H]
    }
}

Intermediate Operations Summary

Operation Signature Description
filter filter(Predicate<T>) Keep elements matching the predicate
map map(Function<T, R>) Transform each element
flatMap flatMap(Function<T, Stream<R>>) Transform and flatten nested streams
sorted sorted() or sorted(Comparator) Sort elements
distinct distinct() Remove duplicates
peek peek(Consumer<T>) Perform action without consuming
limit limit(long n) Truncate to first n elements
skip skip(long n) Discard first n elements

4. Terminal Operations

Terminal operations trigger the execution of the entire pipeline and produce a result (a value, a collection, or a side-effect). Once a terminal operation is called, the stream is consumed and cannot be reused.

4.1 forEach() and forEachOrdered()

forEach() performs an action on each element. For parallel streams, use forEachOrdered() if order matters.

import java.util.List;

public class ForEachExample {
    public static void main(String[] args) {
        List fruits = List.of("Apple", "Banana", "Cherry");

        // forEach -- order guaranteed for sequential streams
        fruits.stream().forEach(f -> System.out.println("Fruit: " + f));

        // Method reference shorthand
        fruits.stream().forEach(System.out::println);

        // forEachOrdered -- guarantees encounter order even in parallel
        fruits.parallelStream().forEachOrdered(System.out::println);
        // Output (always in order): Apple, Banana, Cherry
    }
}

4.2 collect()

collect() is the most versatile terminal operation. It uses a Collector to accumulate elements into a result container (List, Set, Map, String, etc.). The Collectors utility class provides dozens of pre-built collectors.

import java.util.*;
import java.util.stream.Collectors;

public class CollectExample {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "Alice", "David");

        // Collect to List
        List list = names.stream()
            .filter(n -> n.length() > 3)
            .collect(Collectors.toList());
        System.out.println("List: " + list);
        // Output: List: [Alice, Charlie, Alice, David]

        // Collect to Set (removes duplicates)
        Set set = names.stream()
            .collect(Collectors.toSet());
        System.out.println("Set: " + set);

        // Collect to Map
        Map nameLengths = names.stream()
            .distinct()
            .collect(Collectors.toMap(
                name -> name,         // key
                String::length        // value
            ));
        System.out.println("Map: " + nameLengths);
        // Output: Map: {Alice=5, Bob=3, Charlie=7, David=5}

        // Join strings
        String joined = names.stream()
            .distinct()
            .collect(Collectors.joining(", ", "[", "]"));
        System.out.println("Joined: " + joined);
        // Output: Joined: [Alice, Bob, Charlie, David]
    }
}

4.3 reduce()

reduce() combines all elements into a single result by repeatedly applying a binary operator. It has three forms: with identity, without identity, and with identity + combiner (for parallel streams).

import java.util.List;
import java.util.Optional;

public class ReduceExample {
    public static void main(String[] args) {
        List numbers = List.of(1, 2, 3, 4, 5);

        // reduce with identity -- always returns a value
        int sum = numbers.stream()
            .reduce(0, Integer::sum);
        System.out.println("Sum: " + sum); // Output: Sum: 15

        int product = numbers.stream()
            .reduce(1, (a, b) -> a * b);
        System.out.println("Product: " + product); // Output: Product: 120

        // reduce without identity -- returns Optional (stream could be empty)
        Optional max = numbers.stream()
            .reduce(Integer::max);
        max.ifPresent(m -> System.out.println("Max: " + m)); // Output: Max: 5

        // Concatenating strings with reduce
        List words = List.of("Java", "Streams", "Are", "Powerful");
        String sentence = words.stream()
            .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
        System.out.println("Sentence: " + sentence);
        // Output: Sentence: Java Streams Are Powerful

        // reduce on empty stream
        Optional emptyResult = List.of().stream()
            .reduce(Integer::sum);
        System.out.println("Empty: " + emptyResult.isPresent()); // Output: Empty: false
    }
}

4.4 count(), min(), max()

These terminal operations return aggregate values from the stream.

import java.util.Comparator;
import java.util.List;
import java.util.Optional;

public class CountMinMaxExample {
    public static void main(String[] args) {
        List numbers = List.of(5, 3, 8, 1, 9, 2, 7);

        // count()
        long count = numbers.stream()
            .filter(n -> n > 5)
            .count();
        System.out.println("Count > 5: " + count); // Output: Count > 5: 3

        // min() with Comparator
        Optional min = numbers.stream()
            .min(Comparator.naturalOrder());
        System.out.println("Min: " + min.orElse(-1)); // Output: Min: 1

        // max() with Comparator
        Optional max = numbers.stream()
            .max(Comparator.naturalOrder());
        System.out.println("Max: " + max.orElse(-1)); // Output: Max: 9

        // Finding the longest string
        List words = List.of("cat", "elephant", "dog", "hippopotamus");
        Optional longest = words.stream()
            .max(Comparator.comparingInt(String::length));
        System.out.println("Longest: " + longest.orElse("none"));
        // Output: Longest: hippopotamus
    }
}

4.5 findFirst(), findAny()

findFirst() returns the first element. findAny() returns any element (useful with parallel streams for better performance). Both return Optional.

import java.util.List;
import java.util.Optional;

public class FindExample {
    public static void main(String[] args) {
        List names = List.of("Charlie", "Alice", "Bob", "Andrew", "Anna");

        // findFirst() -- guaranteed to return the first matching element
        Optional first = names.stream()
            .filter(n -> n.startsWith("A"))
            .findFirst();
        System.out.println("First with A: " + first.orElse("none"));
        // Output: First with A: Alice

        // findAny() -- may return any matching element (non-deterministic in parallel)
        Optional any = names.parallelStream()
            .filter(n -> n.startsWith("A"))
            .findAny();
        System.out.println("Any with A: " + any.orElse("none"));
        // Output: Any with A: Alice (or Andrew or Anna -- non-deterministic)

        // findFirst on empty result
        Optional missing = names.stream()
            .filter(n -> n.startsWith("Z"))
            .findFirst();
        System.out.println("Found Z? " + missing.isPresent());
        // Output: Found Z? false
    }
}

4.6 anyMatch(), allMatch(), noneMatch()

These short-circuiting operations test whether elements match a condition. They return boolean and stop processing as soon as the answer is determined.

import java.util.List;

public class MatchExample {
    public static void main(String[] args) {
        List numbers = List.of(2, 4, 6, 8, 10);

        // anyMatch -- does at least one element match?
        boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);
        System.out.println("Has even? " + hasEven); // Output: Has even? true

        // allMatch -- do ALL elements match?
        boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0);
        System.out.println("All even? " + allEven); // Output: All even? true

        // noneMatch -- does NO element match?
        boolean noneNegative = numbers.stream().noneMatch(n -> n < 0);
        System.out.println("None negative? " + noneNegative); // Output: None negative? true

        // Short-circuiting behavior
        List mixed = List.of(1, 2, 3, 4, 5);
        boolean anyOver3 = mixed.stream()
            .peek(n -> System.out.print("Checking " + n + "... "))
            .anyMatch(n -> n > 3);
        System.out.println("\nAny over 3? " + anyOver3);
        // Output: Checking 1... Checking 2... Checking 3... Checking 4...
        //         Any over 3? true
        // Note: 5 was never checked -- short-circuited after finding 4
    }
}

4.7 toArray()

toArray() collects the stream elements into an array. Without arguments it returns Object[]; with a generator function it returns the specific type.

import java.util.Arrays;
import java.util.List;

public class ToArrayExample {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie");

        // toArray() without argument -- returns Object[]
        Object[] objArray = names.stream().toArray();
        System.out.println(Arrays.toString(objArray));

        // toArray with generator -- returns String[]
        String[] strArray = names.stream()
            .map(String::toUpperCase)
            .toArray(String[]::new);
        System.out.println(Arrays.toString(strArray));
        // Output: [ALICE, BOB, CHARLIE]

        // Convert filtered stream to int array
        int[] evenNumbers = List.of(1, 2, 3, 4, 5, 6).stream()
            .filter(n -> n % 2 == 0)
            .mapToInt(Integer::intValue)
            .toArray();
        System.out.println(Arrays.toString(evenNumbers));
        // Output: [2, 4, 6]
    }
}

Terminal Operations Summary

Operation Return Type Description
forEach(Consumer) void Perform action on each element
collect(Collector) R Accumulate into a container (List, Set, Map, etc.)
reduce(identity, op) T Combine all elements into one value
count() long Count elements
min(Comparator) Optional<T> Find minimum element
max(Comparator) Optional<T> Find maximum element
findFirst() Optional<T> First element (deterministic)
findAny() Optional<T> Any element (non-deterministic in parallel)
anyMatch(Predicate) boolean True if any element matches
allMatch(Predicate) boolean True if all elements match
noneMatch(Predicate) boolean True if no element matches
toArray() Object[] or T[] Collect to an array

5. Collectors

The Collectors class in java.util.stream provides a rich set of pre-built collectors for the collect() terminal operation. This section covers the most commonly used ones. For a deep dive into all Collectors, see the dedicated Java 8 Collectors Class tutorial.

5.1 Basic Collectors: toList(), toSet(), toMap()

import java.util.*;
import java.util.stream.Collectors;

public class BasicCollectors {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "Alice", "David");

        // toList() -- ordered, allows duplicates
        List list = names.stream()
            .filter(n -> n.length() > 3)
            .collect(Collectors.toList());
        System.out.println("List: " + list);
        // Output: List: [Alice, Charlie, Alice, David]

        // toSet() -- unordered, no duplicates
        Set set = names.stream()
            .collect(Collectors.toSet());
        System.out.println("Set: " + set);

        // toMap() -- key-value pairs
        Map nameLengths = names.stream()
            .distinct()
            .collect(Collectors.toMap(
                n -> n,              // key mapper
                String::length       // value mapper
            ));
        System.out.println("Map: " + nameLengths);

        // toMap() with merge function for duplicate keys
        Map nameCount = names.stream()
            .collect(Collectors.toMap(
                n -> n,              // key
                n -> 1,              // value
                Integer::sum         // merge: add counts
            ));
        System.out.println("Counts: " + nameCount);
        // Output: Counts: {Alice=2, Bob=1, Charlie=1, David=1}
    }
}

5.2 joining()

Collectors.joining() concatenates stream elements into a single String. You can specify a delimiter, prefix, and suffix.

import java.util.List;
import java.util.stream.Collectors;

public class JoiningCollector {
    public static void main(String[] args) {
        List languages = List.of("Java", "Python", "Go", "Rust");

        // Simple concatenation
        String simple = languages.stream().collect(Collectors.joining());
        System.out.println(simple); // Output: JavaPythonGoRust

        // With delimiter
        String delimited = languages.stream().collect(Collectors.joining(", "));
        System.out.println(delimited); // Output: Java, Python, Go, Rust

        // With delimiter, prefix, and suffix
        String formatted = languages.stream()
            .collect(Collectors.joining(" | ", "[ ", " ]"));
        System.out.println(formatted); // Output: [ Java | Python | Go | Rust ]

        // Practical: building a SQL IN clause
        List ids = List.of(101, 205, 310, 422);
        String inClause = ids.stream()
            .map(String::valueOf)
            .collect(Collectors.joining(", ", "WHERE id IN (", ")"));
        System.out.println(inClause);
        // Output: WHERE id IN (101, 205, 310, 422)
    }
}

5.3 groupingBy() and partitioningBy()

groupingBy() groups elements by a classifier function into a Map<K, List<T>>. partitioningBy() is a special case that splits elements into two groups (true/false) based on a predicate.

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class GroupingExample {
    public static void main(String[] args) {
        List names = List.of("Alice", "Anna", "Bob", "Brian", "Charlie", "Chris");

        // Group by first letter
        Map> byLetter = names.stream()
            .collect(Collectors.groupingBy(n -> n.charAt(0)));
        System.out.println("By letter: " + byLetter);
        // Output: By letter: {A=[Alice, Anna], B=[Bob, Brian], C=[Charlie, Chris]}

        // Group by length with counting
        Map byLength = names.stream()
            .collect(Collectors.groupingBy(String::length, Collectors.counting()));
        System.out.println("By length: " + byLength);
        // Output: By length: {3=1, 4=1, 5=3, 7=1}

        // partitioningBy -- split into two groups
        Map> partitioned = names.stream()
            .collect(Collectors.partitioningBy(n -> n.length() > 4));
        System.out.println("Long names: " + partitioned.get(true));
        System.out.println("Short names: " + partitioned.get(false));
        // Output: Long names: [Alice, Brian, Charlie, Chris]
        //         Short names: [Anna, Bob]
    }
}

6. Parallel Streams

Parallel streams divide the workload across multiple threads using the common ForkJoinPool. They can significantly speed up processing of large data sets, but they are not always faster and come with important caveats.

When to Use Parallel Streams

Use Parallel When… Avoid Parallel When…
Large data sets (100,000+ elements) Small data sets (overhead exceeds benefit)
CPU-bound operations (computation) I/O-bound operations (network, file reads)
No shared mutable state Operations modify shared variables
Operations are independent and stateless Order-dependent processing
Source supports efficient splitting (ArrayList, arrays) Poor-splitting sources (LinkedList, Stream.iterate)
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ParallelStreamExample {
    public static void main(String[] args) {
        // Creating a parallel stream
        List numbers = IntStream.rangeClosed(1, 1_000_000)
            .boxed()
            .collect(Collectors.toList());

        // Sequential sum
        long startSeq = System.nanoTime();
        long sumSeq = numbers.stream()
            .mapToLong(Integer::longValue)
            .sum();
        long timeSeq = System.nanoTime() - startSeq;

        // Parallel sum
        long startPar = System.nanoTime();
        long sumPar = numbers.parallelStream()
            .mapToLong(Integer::longValue)
            .sum();
        long timePar = System.nanoTime() - startPar;

        System.out.println("Sequential sum: " + sumSeq + " in " + timeSeq / 1_000_000 + "ms");
        System.out.println("Parallel sum:   " + sumPar + " in " + timePar / 1_000_000 + "ms");

        // Converting between sequential and parallel
        List names = List.of("Alice", "Bob", "Charlie");
        names.stream()
            .parallel()    // switch to parallel
            .sequential()  // switch back to sequential
            .forEach(System.out::println);
    }
}

Thread Safety Warning

Parallel streams use multiple threads. If your operations modify shared mutable state, you will get race conditions and incorrect results. Here is an example of what NOT to do:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ParallelSafetyExample {
    public static void main(String[] args) {
        // WRONG: Modifying a shared list from parallel stream
        List unsafeList = new ArrayList<>();
        IntStream.rangeClosed(1, 10000)
            .parallel()
            .forEach(unsafeList::add); // Race condition!
        System.out.println("Unsafe size: " + unsafeList.size());
        // Output: unpredictable -- could be less than 10000, could throw exception

        // CORRECT: Use collect() instead of modifying shared state
        List safeList = IntStream.rangeClosed(1, 10000)
            .parallel()
            .boxed()
            .collect(Collectors.toList());
        System.out.println("Safe size: " + safeList.size());
        // Output: Safe size: 10000

        // CORRECT: Use a synchronized collection if you must
        List syncList = Collections.synchronizedList(new ArrayList<>());
        IntStream.rangeClosed(1, 10000)
            .parallel()
            .forEach(syncList::add);
        System.out.println("Sync size: " + syncList.size());
        // Output: Sync size: 10000
    }
}

7. Primitive Streams

Java generics do not work with primitives, so Stream<int> is not valid. To avoid the performance cost of boxing/unboxing (wrapping primitives in Integer, Long, Double), Java provides three specialized stream types:

Stream Type Element Type Key Methods
IntStream int sum(), average(), min(), max(), range()
LongStream long sum(), average(), min(), max(), range()
DoubleStream double sum(), average(), min(), max()

Converting Between Object and Primitive Streams

From To Method
Stream<T> IntStream mapToInt(ToIntFunction)
Stream<T> LongStream mapToLong(ToLongFunction)
Stream<T> DoubleStream mapToDouble(ToDoubleFunction)
IntStream Stream<Integer> boxed()
IntStream LongStream asLongStream()
IntStream DoubleStream asDoubleStream()
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.stream.IntStream;

public class PrimitiveStreamExample {
    public static void main(String[] args) {
        // IntStream basics
        int sum = IntStream.of(10, 20, 30, 40, 50).sum();
        System.out.println("Sum: " + sum); // Output: Sum: 150

        OptionalDouble avg = IntStream.of(10, 20, 30, 40, 50).average();
        System.out.println("Average: " + avg.orElse(0.0)); // Output: Average: 30.0

        OptionalInt min = IntStream.of(10, 20, 30, 40, 50).min();
        System.out.println("Min: " + min.orElse(-1)); // Output: Min: 10

        OptionalInt max = IntStream.of(10, 20, 30, 40, 50).max();
        System.out.println("Max: " + max.orElse(-1)); // Output: Max: 50

        // summaryStatistics -- get all stats in one pass
        IntSummaryStatistics stats = IntStream.of(10, 20, 30, 40, 50)
            .summaryStatistics();
        System.out.println("Count: " + stats.getCount());     // 5
        System.out.println("Sum: " + stats.getSum());         // 150
        System.out.println("Min: " + stats.getMin());         // 10
        System.out.println("Max: " + stats.getMax());         // 50
        System.out.println("Avg: " + stats.getAverage());     // 30.0

        // mapToInt -- convert Stream to IntStream
        List words = List.of("Java", "is", "powerful");
        int totalChars = words.stream()
            .mapToInt(String::length)
            .sum();
        System.out.println("Total chars: " + totalChars); // Output: Total chars: 15

        // boxed() -- convert IntStream back to Stream
        List boxed = IntStream.rangeClosed(1, 5)
            .boxed()
            .toList();
        System.out.println("Boxed: " + boxed); // Output: Boxed: [1, 2, 3, 4, 5]
    }
}

8. Stream Pipeline Best Practices

Streams are a powerful tool, but they can be misused. Following these best practices will keep your stream pipelines readable, correct, and performant.

8.1 Keep Operations Simple

Each operation in the pipeline should do one thing. If a lambda is more than 2-3 lines, extract it into a method and use a method reference.

import java.util.List;
import java.util.stream.Collectors;

public class BestPracticeSimple {
    public static void main(String[] args) {
        List emails = List.of(
            "alice@GMAIL.com", "  bob@yahoo.com  ", "CHARLIE@gmail.com", "invalid-email"
        );

        // BAD: Complex logic crammed into one lambda
        List bad = emails.stream()
            .filter(e -> {
                String trimmed = e.trim().toLowerCase();
                return trimmed.contains("@") && trimmed.endsWith(".com");
            })
            .map(e -> e.trim().toLowerCase())
            .collect(Collectors.toList());

        // GOOD: Extract complex logic into named methods
        List good = emails.stream()
            .map(String::trim)
            .map(String::toLowerCase)
            .filter(BestPracticeSimple::isValidEmail)
            .collect(Collectors.toList());

        System.out.println("Valid emails: " + good);
        // Output: Valid emails: [alice@gmail.com, bob@yahoo.com, charlie@gmail.com]
    }

    private static boolean isValidEmail(String email) {
        return email.contains("@") && email.endsWith(".com");
    }
}

8.2 Avoid Side Effects

Stream operations should be pure functions — they should not modify external state. The exception is forEach(), which exists specifically for side effects.

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class BestPracticeNoSideEffects {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie");

        // BAD: Side effect in map() -- modifying an external list
        List external = new ArrayList<>();
        names.stream()
            .map(n -> {
                external.add(n); // Side effect -- do not do this!
                return n.toUpperCase();
            })
            .collect(Collectors.toList());

        // GOOD: Use collect() to build the result
        List result = names.stream()
            .map(String::toUpperCase)
            .collect(Collectors.toList());
        System.out.println("Result: " + result);
        // Output: Result: [ALICE, BOB, CHARLIE]
    }
}

8.3 Do Not Reuse Streams

A Stream can only be consumed once. Attempting to reuse it throws IllegalStateException. If you need to process the same data multiple times, create a new Stream each time or store the source data.

import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class BestPracticeNoReuse {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie");

        // BAD: Reusing a stream
        Stream stream = names.stream().filter(n -> n.length() > 3);
        stream.forEach(System.out::println);  // works
        // stream.count();  // IllegalStateException: stream has already been operated upon

        // GOOD: Create a new stream each time
        long count = names.stream().filter(n -> n.length() > 3).count();
        List filtered = names.stream()
            .filter(n -> n.length() > 3)
            .collect(Collectors.toList());

        // ALTERNATIVE: Use a Supplier for repeated use
        Supplier> streamSupplier = () ->
            names.stream().filter(n -> n.length() > 3);

        long count2 = streamSupplier.get().count();
        List list2 = streamSupplier.get().collect(Collectors.toList());
        System.out.println("Count: " + count2 + ", List: " + list2);
    }
}

8.4 Order of Operations Matters

The order of intermediate operations can significantly affect performance. Filter early to reduce the number of elements processed by expensive operations like sorted() or map().

import java.util.List;
import java.util.stream.Collectors;

public class BestPracticeOrder {
    public static void main(String[] args) {
        List names = List.of("Charlie", "Alice", "Eve", "Bob", "David",
            "Frank", "Grace", "Hannah", "Ivy", "Jack");

        // BAD: Sort everything, then filter
        List bad = names.stream()
            .sorted()                            // sorts all 10 elements
            .filter(n -> n.length() > 3)         // then filters
            .map(String::toUpperCase)
            .collect(Collectors.toList());

        // GOOD: Filter first, then sort the smaller set
        List good = names.stream()
            .filter(n -> n.length() > 3)         // filters to 7 elements
            .sorted()                            // sorts only 7 elements
            .map(String::toUpperCase)
            .collect(Collectors.toList());

        System.out.println("Result: " + good);
        // Output: Result: [ALICE, CHARLIE, DAVID, FRANK, GRACE, HANNAH]
    }
}

9. Stream vs Loop

Streams and loops are both tools for processing collections. Neither is universally better — each has strengths. Knowing when to use which is a mark of a senior developer.

When Streams Shine

Scenario Why Streams Win
Filter + transform + collect Readable, composable pipeline
Grouping and aggregation groupingBy, counting, summarizing are expressive
Parallel processing One method call to parallelize
Chaining multiple operations Flat pipeline vs nested loops
Declarative data queries Reads like SQL: filter-where, map-select, collect-into

When Loops Are Better

Scenario Why Loops Win
Need to break/continue mid-iteration Streams have no break/continue
Modifying the source collection Streams do not modify the source
Index-based access needed Streams do not expose indices directly
Multiple return values from one pass Loops can update multiple variables
Checked exceptions in the logic Lambdas do not support checked exceptions neatly
Simple iteration with nothing to transform A for-each loop is simpler
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class StreamVsLoop {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "Anna", "David", "Andrew");

        // --- STREAM WINS: Filter + Transform + Collect ---

        // Stream approach -- clean, declarative
        List streamResult = names.stream()
            .filter(n -> n.startsWith("A"))
            .map(String::toUpperCase)
            .sorted()
            .collect(Collectors.toList());

        // Loop approach -- more verbose
        List loopResult = new ArrayList<>();
        for (String name : names) {
            if (name.startsWith("A")) {
                loopResult.add(name.toUpperCase());
            }
        }
        java.util.Collections.sort(loopResult);

        System.out.println("Stream: " + streamResult);
        System.out.println("Loop:   " + loopResult);

        // --- STREAM WINS: Grouping ---
        Map> grouped = names.stream()
            .collect(Collectors.groupingBy(n -> n.charAt(0)));
        System.out.println("Grouped: " + grouped);
        // This would take 10+ lines with loops

        // --- LOOP WINS: Early exit with break ---
        String found = null;
        for (String name : names) {
            if (name.length() > 5) {
                found = name;
                break; // Stream alternative: findFirst(), but less flexible
            }
        }
        System.out.println("Found: " + found);

        // --- LOOP WINS: Index-based access ---
        for (int i = 0; i < names.size(); i++) {
            System.out.println(i + ": " + names.get(i));
        }
    }
}

10. Complete Practical Example: Employee Analytics

Let us bring everything together with a realistic example. We have a list of employees and need to perform various analytics: filtering, grouping, aggregating, and reporting.

import java.util.*;
import java.util.stream.Collectors;

public class EmployeeAnalytics {

    enum Department { ENGINEERING, MARKETING, SALES, HR, FINANCE }

    record Employee(String name, Department department, double salary, int yearsOfExperience) {}

    public static void main(String[] args) {
        List employees = List.of(
            new Employee("Alice",   Department.ENGINEERING, 95000, 5),
            new Employee("Bob",     Department.ENGINEERING, 110000, 8),
            new Employee("Charlie", Department.MARKETING,   72000, 3),
            new Employee("Diana",   Department.SALES,       68000, 2),
            new Employee("Eve",     Department.ENGINEERING, 125000, 12),
            new Employee("Frank",   Department.HR,          65000, 4),
            new Employee("Grace",   Department.FINANCE,     88000, 6),
            new Employee("Hannah",  Department.MARKETING,   78000, 5),
            new Employee("Ivan",    Department.SALES,       71000, 3),
            new Employee("Julia",   Department.FINANCE,     92000, 7),
            new Employee("Kevin",   Department.HR,          60000, 1),
            new Employee("Laura",   Department.ENGINEERING, 105000, 9),
            new Employee("Mike",    Department.SALES,       75000, 4),
            new Employee("Nina",    Department.MARKETING,   82000, 6),
            new Employee("Oscar",   Department.FINANCE,     97000, 8)
        );

        // 1. Total salary expense
        double totalSalary = employees.stream()
            .mapToDouble(Employee::salary)
            .sum();
        System.out.println("Total salary expense: $" + String.format("%,.2f", totalSalary));

        // 2. Average salary
        double avgSalary = employees.stream()
            .mapToDouble(Employee::salary)
            .average()
            .orElse(0.0);
        System.out.println("Average salary: $" + String.format("%,.2f", avgSalary));

        // 3. Highest paid employee
        employees.stream()
            .max(Comparator.comparingDouble(Employee::salary))
            .ifPresent(e -> System.out.println("Highest paid: " + e.name() + " ($" +
                String.format("%,.2f", e.salary()) + ")"));

        // 4. Employees earning above average
        List aboveAverage = employees.stream()
            .filter(e -> e.salary() > avgSalary)
            .sorted(Comparator.comparingDouble(Employee::salary).reversed())
            .map(e -> e.name() + " ($" + String.format("%,.2f", e.salary()) + ")")
            .collect(Collectors.toList());
        System.out.println("Above average: " + aboveAverage);

        // 5. Group by department
        Map> byDept = employees.stream()
            .collect(Collectors.groupingBy(
                Employee::department,
                Collectors.mapping(Employee::name, Collectors.toList())
            ));
        System.out.println("\nEmployees by department:");
        byDept.forEach((dept, names) ->
            System.out.println("  " + dept + ": " + names));

        // 6. Average salary by department
        Map avgByDept = employees.stream()
            .collect(Collectors.groupingBy(
                Employee::department,
                Collectors.averagingDouble(Employee::salary)
            ));
        System.out.println("\nAverage salary by department:");
        avgByDept.entrySet().stream()
            .sorted(Map.Entry.comparingByValue().reversed())
            .forEach(e -> System.out.println("  " + e.getKey() + ": $" +
                String.format("%,.2f", e.getValue())));

        // 7. Department with highest average salary
        avgByDept.entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .ifPresent(e -> System.out.println("\nHighest avg dept: " + e.getKey() +
                " ($" + String.format("%,.2f", e.getValue()) + ")"));

        // 8. Count employees per department
        Map countByDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::department, Collectors.counting()));
        System.out.println("\nHeadcount by department: " + countByDept);

        // 9. Partition into senior (5+ years) and junior
        Map> seniorPartition = employees.stream()
            .collect(Collectors.partitioningBy(e -> e.yearsOfExperience() >= 5));
        System.out.println("\nSenior employees (5+ years):");
        seniorPartition.get(true).forEach(e ->
            System.out.println("  " + e.name() + " - " + e.yearsOfExperience() + " years"));
        System.out.println("Junior employees (< 5 years):");
        seniorPartition.get(false).forEach(e ->
            System.out.println("  " + e.name() + " - " + e.yearsOfExperience() + " years"));

        // 10. Salary statistics by department
        System.out.println("\nSalary statistics by department:");
        employees.stream()
            .collect(Collectors.groupingBy(
                Employee::department,
                Collectors.summarizingDouble(Employee::salary)
            ))
            .forEach((dept, stats) -> System.out.printf(
                "  %s: count=%d, min=$%,.0f, max=$%,.0f, avg=$%,.0f, total=$%,.0f%n",
                dept, stats.getCount(), stats.getMin(), stats.getMax(),
                stats.getAverage(), stats.getSum()));

        // 11. Top 3 earners across the company
        System.out.println("\nTop 3 earners:");
        employees.stream()
            .sorted(Comparator.comparingDouble(Employee::salary).reversed())
            .limit(3)
            .forEach(e -> System.out.println("  " + e.name() + " - $" +
                String.format("%,.2f", e.salary())));

        // 12. Build a salary report string
        String report = employees.stream()
            .sorted(Comparator.comparing(Employee::name))
            .map(e -> String.format("%-10s | %-12s | $%,10.2f | %2d yrs",
                e.name(), e.department(), e.salary(), e.yearsOfExperience()))
            .collect(Collectors.joining("\n"));
        System.out.println("\n--- Employee Report ---");
        System.out.println(String.format("%-10s | %-12s | %11s | %s",
            "Name", "Department", "Salary", "Experience"));
        System.out.println("-".repeat(55));
        System.out.println(report);
    }
}

Expected Output (partial)

Total salary expense: $1,283,000.00
Average salary: $85,533.33
Highest paid: Eve ($125,000.00)
Above average: [Eve ($125,000.00), Bob ($110,000.00), Laura ($105,000.00), Oscar ($97,000.00), Alice ($95,000.00), Julia ($92,000.00), Grace ($88,000.00)]

Employees by department:
  ENGINEERING: [Alice, Bob, Eve, Laura]
  MARKETING: [Charlie, Hannah, Nina]
  SALES: [Diana, Ivan, Mike]
  HR: [Frank, Kevin]
  FINANCE: [Grace, Julia, Oscar]

Average salary by department:
  ENGINEERING: $108,750.00
  FINANCE: $92,333.33
  MARKETING: $77,333.33
  SALES: $71,333.33
  HR: $62,500.00

Top 3 earners:
  Eve - $125,000.00
  Bob - $110,000.00
  Laura - $105,000.00

Key Takeaways

  • Streams are declarative -- you describe what you want, not how to iterate.
  • Pipelines are composable -- chain filter, map, sort, and collect in a readable flow.
  • Lazy evaluation means only the necessary work is performed.
  • Collectors are the Swiss army knife for aggregation -- grouping, counting, averaging, joining.
  • Parallel streams are powerful but require thread-safe, stateless operations.
  • Primitive streams avoid boxing overhead for numeric operations.
  • Use streams for data transformation and loops when you need fine-grained control.



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *