Skip to main content

Streams API in Java

  • The Stream API, introduced in Java 8, provides a modern and efficient way to process collections of data.

  • It allows you to write concise and readable code to perform operations such as filtering, mapping, and reducing on collections.

  • Streams represent a sequence of elements and support many convenient methods for data manipulation.

Why Streams?

  1. Declarative Programming:

    • Streams enable a more declarative style of programming. Instead of specifying detailed steps on how to achieve a result, you express what you want to achieve, allowing the underlying implementation to handle the intricacies.
  2. Readability and Conciseness:

    • Streams provide a more readable and concise syntax compared to traditional iterative approaches. The code is often more expressive and resembles the problem statement closely.
  3. Parallelism:

    • Streams are designed with parallelism in mind. Many stream operations can automatically take advantage of multiple cores, providing a convenient way to parallelize computations without dealing with low-level threading details.
  4. Lazy Evaluation:

    • Streams support lazy evaluation, meaning that elements are computed on-demand and only as much as needed. This can lead to more efficient use of resources, especially when dealing with large datasets.

Where Streams Fit In:

  1. Data Processing:

    • Streams are particularly useful when dealing with data processing tasks, such as filtering, mapping, and reducing collections of data. They allow for expressing these operations in a concise and expressive way.
  2. Functional Transformations:

    • When applying functional transformations to collections, streams become a powerful tool. Operations like map, filter, and reduce can be seamlessly applied, leading to more readable code.
  3. Parallel Computing:

    • Streams provide an easy path to parallelism. By leveraging parallel stream operations, developers can take advantage of multi-core processors without delving into the complexities of explicit thread management.

Creating a Stream

Streams can be created from various data sources, such as collections, arrays, or I/O channels.

From Collections

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

public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> stream = names.stream();

// Process the stream
stream.forEach(System.out::println);
}
}

From Arrays

import java.util.stream.Stream;

public class ArrayStreamExample {
public static void main(String[] args) {
String[] names = {"Alice", "Bob", "Charlie"};
Stream<String> stream = Stream.of(names);

// Process the stream
stream.forEach(System.out::println);
}
}

Intermediate Operations

Intermediate operations return a new stream. These operations are lazy and do not execute until a terminal operation is invoked.

Common Intermediate Operations

  1. filter: Filters elements based on a predicate.
Stream<String> filteredStream = stream.filter(name -> name.startsWith("A"));
  1. map: Transforms elements using a function.
Stream<Integer> lengthStream = stream.map(String::length);
  1. sorted: Sorts the elements of the stream.
Stream<String> sortedStream = stream.sorted();

Terminal Operations

Terminal operations produce a result or side-effect and trigger the processing of the stream.

Common Terminal Operations

  1. collect: Collects the elements of the stream into a collection.
List<String> nameList = stream.collect(Collectors.toList());
  1. forEach: Performs an action for each element of the stream.
stream.forEach(System.out::println);
  1. reduce: Combines the elements of the stream using an associative accumulation function.
int sum = stream.reduce(0, Integer::sum);

Example Code: A Simple Stream in Action

Let's consider a scenario where we have a list of numbers, and we want to find the sum of the squares of the even numbers using traditional iteration and then using streams.

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

public class StreamExample {
public static void main(String[] args) {
// Traditional approach
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

int sumOfSquares = 0;
for (int num : numbers) {
if (num % 2 == 0) {
sumOfSquares += num * num;
}
}
System.out.println("Sum of squares (traditional): " + sumOfSquares);

// Using Streams
int sumOfSquaresStream = numbers.stream()
.filter(num -> num % 2 == 0)
.mapToInt(num -> num * num)
.sum();
System.out.println("Sum of squares (using streams): " + sumOfSquaresStream);
}
}

Output

Sum of squares (traditional): 220
Sum of squares (using streams): 220

Explanation

This code demonstrates two approaches to calculate the sum of squares of even numbers from a list: the traditional approach and using the Stream API.

  1. Traditional Approach

    • List of Numbers: Create a list of integers from 1 to 10.

    • Initialize Sum: Set sumOfSquares to 0 to store the result.

    • Loop Through Numbers: Iterate over each number in the list.

    • Filter Even Numbers: Check if the number is even.

    • Calculate Square and Add to Sum: Square the even number and add it to sumOfSquares.

    • Print Result: Output the result.

  1. Stream Approach

    • Create Stream: Convert the list to a stream.

    • Filter Even Numbers: Use filter to keep only even numbers.

    • Map to Squares: Use mapToInt to square each even number.

    • Sum the Squares: Use sum to calculate the sum of the squared numbers.

    • Print Result: Output the result.

Example: Stream API in Action

Here's a complete example demonstrating the use of Stream API to filter, map, and collect data from a list:

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

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

// Filter names that start with 'A'
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());

// Print the filtered names
System.out.println("Filtered Names: " + filteredNames);

// Map names to their lengths
List<Integer> nameLengths = names.stream()
.map(String::length)
.collect(Collectors.toList());

// Print the lengths of names
System.out.println("Name Lengths: " + nameLengths);

// Sort names in reverse order
List<String> sortedNames = names.stream()
.sorted((a, b) -> b.compareTo(a))
.collect(Collectors.toList());

// Print the sorted names
System.out.println("Sorted Names: " + sortedNames);
}
}

Output

Filtered Names: [Alice]
Name Lengths: [5, 3, 7, 5, 6]
Sorted Names: [Edward, David, Charlie, Bob, Alice]

Explanation

This code demonstrates three common operations using the Stream API: filtering, mapping, and sorting.

  1. List of Names: A list of names is created.

    • List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Edward");
  2. Filtering Names:

    • Purpose: To get a list of names that start with the letter 'A'.

    • Operation: Uses filter to check each name and collect to gather the results into a new list.

    • List<String> filteredNames = names.stream().filter(name -> name.startsWith("A")).collect(Collectors.toList());

    • Result: Prints Filtered Names: [Alice].

  3. Mapping Names to Lengths:

    • Purpose: To get a list of the lengths of each name.

    • Operation: Uses map to transform each name to its length and collect to gather the results into a new list.

    • List<Integer> nameLengths = names.stream().map(String::length).collect(Collectors.toList());

    • Result: Prints Name Lengths: [5, 3, 7, 5, 6].

  4. Sorting Names in Reverse Order:

    • Purpose: To sort the names in reverse alphabetical order.

    • Operation: Uses sorted with a custom comparator to reverse the order and collect to gather the results into a new list.

    • List<String> sortedNames = names.stream().sorted((a, b) -> b.compareTo(a)).collect(Collectors.toList());

    • Result: Prints Sorted Names: [Edward, David, Charlie, Bob, Alice].

Summary

  • Stream API: Introduced in Java 8, provides functional-style operations on collections of data.

  • Key Features: Functional-style operations, lazy evaluation, parallel processing, and pipelining.

  • Components: Includes streams, intermediate operations, and terminal operations.

  • Creating Streams: Can be created from collections, arrays, etc.

  • Intermediate Operations: Such as filter, map, and sorted are lazy and return a new stream.

  • Terminal Operations: Such as collect, forEach, and reduce produce results or side-effects and trigger stream processing.

Understanding the Stream API allows you to write more concise, readable, and efficient code by utilizing functional programming techniques.