Introduction to the Streams API

Published: February 28, 2016


Introduction

The Streams API, introduced in Java 8, is a powerful feature that allows you to process sequences of data in a functional style. With the Streams API, you can efficiently perform operations on collections, such as filtering, mapping, and reducing, without explicitly iterating through them.

In this tutorial, we’ll give an overview of the Streams API, covering its core concepts and providing examples of how to use it effectively.


What Is a Stream?

Stream is a sequence of elements that supports various methods to process them. It is not a data structure (like a list or set) but rather a sequence that can be consumed and transformed.

There are two main types of streams:

  • Sequential Streams – Operates on the data sequentially, one element at a time.
  • Parallel Streams – Operates on the data concurrently, dividing the work across multiple threads.

Streams vs Collections

While collections are data structures (e.g., List, Set), streams are designed to be used for processing data in a declarative way. A stream is always derived from a collection or array.

Here’s a simple example comparing traditional iteration with streams:

Without Streams (Traditional Way):

import java.util.*;

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

        for (String name : names) {
            if (name.startsWith("A")) {
                System.out.println(name);
            }
        }
    }
}

Using Streams:

import java.util.*;

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

        names.stream()
             .filter(name -> name.startsWith("A"))
             .forEach(System.out::println);  // Output: Alice
    }
}

As you can see, the stream approach is more concise and declarative.


Basic Operations on Streams

Streams provide a variety of operations for processing data. These operations can be categorized into two types:

  1. Intermediate Operations – These operations return a new stream and are lazy (they are not executed until a terminal operation is invoked).
  2. Terminal Operations – These operations produce a result (e.g., a collection, a single value) and trigger the execution of the stream pipeline.

Intermediate Operations:

Here are some common intermediate operations:

  1. filter() – Filters elements based on a condition.
  2. map() – Transforms each element.
  3. sorted() – Sorts elements in natural order or based on a comparator.
  4. distinct() – Removes duplicates.

Example: Using filter() and map() to process a list of names.

import java.util.*;

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

        names.stream()
             .filter(name -> name.startsWith("A"))  // Filter names starting with "A"
             .map(String::toUpperCase)  // Convert to uppercase
             .distinct()  // Remove duplicates
             .forEach(System.out::println);  // Output: ALICE
    }
}

In this example:

  • filter() filters the names starting with the letter “A”.
  • map() transforms the names to uppercase.
  • distinct() removes duplicates from the stream.

Terminal Operations:

Here are some common terminal operations:

  1. forEach() – Applies a consumer to each element.
  2. collect() – Collects the results into a collection (e.g., List, Set).
  3. reduce() – Reduces the stream to a single value based on an operation.
  4. count() – Counts the number of elements.
  5. anyMatch()allMatch()noneMatch() – Checks conditions for elements in the stream.

Example: Using collect() to create a list of names that start with “C”.

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

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

        List<String> filteredNames = names.stream()
                                          .filter(name -> name.startsWith("C"))
                                          .collect(Collectors.toList());  // Collect into a List

        filteredNames.forEach(System.out::println);  // Output: Charlie
    }
}

Here:

  • collect() is used with the Collectors.toList() method to collect the filtered names into a new list.

Working with Primitive Streams

Java 8 also introduced primitive streams to improve performance when working with primitive types (int, long, double). These streams avoid the overhead of boxing and unboxing.

Example: Using an IntStream to sum numbers in a range:

import java.util.stream.*;

public class PrimitiveStreamExample {
    public static void main(String[] args) {
        int sum = IntStream.range(1, 11)  // Create an IntStream from 1 to 10
                           .sum();  // Sum all the numbers

        System.out.println("Sum: " + sum);  // Output: Sum: 55
    }
}

In this example, the IntStream.range() method creates a stream of integers, and the sum() method calculates the sum.


Parallel Streams

Parallel streams allow you to process data concurrently, leveraging multiple processor cores. This can significantly improve performance for large datasets.

Example: Using a parallel stream to filter and sum even numbers:

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

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

        int sum = numbers.parallelStream()  // Use parallel stream
                         .filter(n -> n % 2 == 0)  // Filter even numbers
                         .mapToInt(Integer::intValue)  // Convert to primitive int
                         .sum();  // Sum the numbers

        System.out.println("Sum of even numbers: " + sum);  // Output: Sum of even numbers: 30
    }
}

Here, the parallelStream() method allows the stream to be processed in parallel, making the operation potentially faster for large datasets.


Summary

The Streams API is a powerful addition to Java that allows developers to process data more efficiently and concisely. In this tutorial, we’ve explored:

  • The basic concepts of streams, including sequential and parallel streams.
  • Common intermediate operations such as filter()map(), and sorted().
  • Terminal operations like forEach()collect(), and reduce().
  • Working with primitive streams for better performance.
  • Using parallel streams for concurrent data processing.

In the next tutorial, we will dive deeper into filtering and sorting data using streams. Stay tuned!

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

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