Skip to main content

7. Java Streams Overview

Java Streams are a powerful feature introduced in Java 8 that simplify processing sequences of elements, such as collections, arrays, or I/O channels. Streams allow you to perform operations like filtering, mapping, and reducing in a more declarative and functional programming style. Unlike collections, streams do not store elements; instead, they convey them from a source and process them through a pipeline of intermediate and terminal operations.

Key Characteristics of Streams

  1. Lazy Evaluation: Intermediate operations are not executed until a terminal operation is invoked. This improves efficiency.
  2. Functional Programming: Streams promote functional programming by using lambda expressions and method references.
  3. Stateless: Most stream operations do not modify the underlying data source (they return new streams or results).

Stream Operations

Stream operations are divided into two main categories:

1. Intermediate Operations

  • These return a new stream and are lazy. They are chained together and executed when a terminal operation is called.
  • Common intermediate operations:
    • filter(): Filters elements based on a predicate.
    • map(): Transforms each element of the stream using a function.
    • sorted(): Sorts the elements of the stream.
    • distinct(): Removes duplicate elements.
    • limit(): Limits the number of elements in the stream.
    • skip(): Skips the first n elements.

2. Terminal Operations

  • These trigger the execution of all intermediate operations and return a result.
  • Common terminal operations:
    • forEach(): Applies an action to each element of the stream.
    • collect(): Collects the result of the stream into a collection (e.g., List, Set, etc.).
    • reduce(): Reduces the stream to a single value using a binary operator (e.g., sum of elements).
    • count(): Returns the count of elements.
    • findFirst() / findAny(): Returns the first or any element.
    • allMatch() / anyMatch() / noneMatch(): Checks if all, any, or none of the elements match a given predicate.

Example Usage

1. Filtering and Mapping

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

// Filter names starting with "A" and convert to uppercase
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());

System.out.println(filteredNames); // Output: [ALICE]

2. Reducing a Stream

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

// Sum of elements using reduce()
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);

System.out.println(sum); // Output: 15

3. Sorting and Limiting

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

// Get top 3 smallest numbers
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.limit(3)
.collect(Collectors.toList());

System.out.println(sortedNumbers); // Output: [1, 2, 3]

Most Commonly Used Stream Commands

  1. filter(Predicate): Filters elements based on a condition.
  • Example: filter(n -> n > 10)
  1. map(Function): Transforms elements into another form.
  • Example: map(n -> n * 2)
  1. collect(Collector): Collects elements into a collection like List, Set, or Map.
  • Example: collect(Collectors.toList())
  1. forEach(Consumer): Performs an action on each element.
  • Example: forEach(System.out::println)
  1. reduce(BinaryOperator): Reduces the elements into a single value.
  • Example: reduce(0, (a, b) -> a + b)
  1. sorted(): Sorts the elements either naturally or by a custom comparator.
  • Example: sorted(Comparator.naturalOrder())
  1. distinct(): Removes duplicates from the stream.
  • Example: distinct()
  1. findFirst() and findAny(): Finds the first or any element.
  • Example: findFirst().orElse("None")
  1. count(): Counts the number of elements.
  • Example: count()
  1. allMatch(), anyMatch(), noneMatch(): Tests conditions on elements.
  • Example: allMatch(n -> n > 0)

Example Workflow: Filtering, Mapping, and Collecting

List<String> words = Arrays.asList("apple", "banana", "cherry", "apple");

// Filter words longer than 5 characters, make them uppercase, and collect to a set
Set<String> result = words.stream()
.filter(word -> word.length() > 5)
.map(String::toUpperCase)
.collect(Collectors.toSet());

System.out.println(result); // Output: [BANANA, CHERRY]

Parallel Streams

Java also provides parallel streams for multi-threaded processing, improving performance in some scenarios (e.g., large data sets):

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

// Use parallelStream for parallel processing
int sum = numbers.parallelStream()
.reduce(0, Integer::sum);

Collectors

In addition to the commonly used Collectors.toList() and Collectors.toSet(), Java’s Collectors class offers several other useful methods that can be applied to Streams for a variety of purposes. These collectors help in performing tasks such as joining elements, grouping, partitioning, or computing statistics.

Here are some useful collectors that you can leverage:

CollectorDescription
toList()Collects elements into a List.
toSet()Collects elements into a Set.
toMap()Collects elements into a Map.
joining()Joins elements into a String.
counting()Counts elements in the stream.
summingInt()Sums integer values in the stream.
averagingInt()Averages integer values in the stream.
maxBy() / minBy()Finds the maximum or minimum element.
groupingBy()Groups elements by a classifier function.
partitioningBy()Partitions elements based on a predicate.
reducing()Performs a reduction on the elements.
toCollection()Collects elements into a specific collection.
summarizingInt()Provides statistics on elements (count, sum, min, avg, max).

Key Points to Remember for Interviews:

  • Streams are immutable: They do not modify the original source; they return a new stream or result.
  • Streams are lazy: Intermediate operations like filter() and map() are not executed until a terminal operation is invoked.
  • Efficiency: Because of lazy evaluation, streams can improve performance by reducing unnecessary computations.
  • Parallelism: Parallel streams can be used to process data in multiple threads for performance gains in the right scenarios.