How to use Stream API to process collections in Java

JavaJavaBeginner
Practice Now

Introduction

The Java Stream API provides a powerful and flexible way to process collections of data. In this tutorial, we will explore how to use the Stream API to perform common operations and transformations on collections, enabling you to write more concise, readable, and efficient Java code.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL java(("`Java`")) -.-> java/ProgrammingTechniquesGroup(["`Programming Techniques`"]) java(("`Java`")) -.-> java/FileandIOManagementGroup(["`File and I/O Management`"]) java(("`Java`")) -.-> java/ConcurrentandNetworkProgrammingGroup(["`Concurrent and Network Programming`"]) java(("`Java`")) -.-> java/DataStructuresGroup(["`Data Structures`"]) java/ProgrammingTechniquesGroup -.-> java/method_overloading("`Method Overloading`") java/FileandIOManagementGroup -.-> java/stream("`Stream`") java/FileandIOManagementGroup -.-> java/io("`IO`") java/ConcurrentandNetworkProgrammingGroup -.-> java/working("`Working`") java/DataStructuresGroup -.-> java/collections_methods("`Collections Methods`") subgraph Lab Skills java/method_overloading -.-> lab-415588{{"`How to use Stream API to process collections in Java`"}} java/stream -.-> lab-415588{{"`How to use Stream API to process collections in Java`"}} java/io -.-> lab-415588{{"`How to use Stream API to process collections in Java`"}} java/working -.-> lab-415588{{"`How to use Stream API to process collections in Java`"}} java/collections_methods -.-> lab-415588{{"`How to use Stream API to process collections in Java`"}} end

Understanding Java Stream API

Java Stream API is a powerful feature introduced in Java 8 that allows you to process collections of data in a more declarative and functional style. Streams provide a way to perform various operations on collections, such as filtering, mapping, sorting, and reducing, without the need for explicit iteration or mutable state.

What is a Java Stream?

A Java Stream is a sequence of elements that supports various operations that can be performed in a pipeline-like fashion. Streams are not data structures themselves, but rather they operate on top of existing data structures, such as collections, arrays, or I/O resources.

Streams provide a high-level abstraction for working with collections, allowing you to focus on the what (the desired outcome) rather than the how (the implementation details).

Key Characteristics of Java Streams

  1. Laziness: Streams are lazy, meaning that they only perform the necessary computations when an operation is requested. This can lead to improved performance and reduced memory usage.

  2. Functional: Streams promote a functional programming style, where operations are defined as pure functions that transform the input data into the desired output.

  3. Parallelism: Streams can be easily parallelized, allowing for efficient utilization of multi-core processors and improved performance on large datasets.

  4. Fluent API: Streams provide a fluent API, where multiple operations can be chained together to create complex data processing pipelines.

Anatomy of a Stream Pipeline

A typical stream pipeline consists of three main parts:

  1. Source: The initial data source, such as a collection, array, or I/O resource.
  2. Intermediate Operations: One or more operations that transform the data, such as filtering, mapping, or sorting.
  3. Terminal Operation: The final operation that produces the desired result, such as reducing, collecting, or iterating.

Here's an example of a stream pipeline that filters, maps, and reduces a list of numbers:

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

int sum = numbers.stream()
                   .filter(n -> n % 2 == 0)
                   .map(n -> n * 2)
                   .reduce(0, Integer::sum);

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

In the next section, we'll dive deeper into the common stream operations and transformations you can use to process your data.

Common Stream Operations and Transformations

Java Streams provide a wide range of operations and transformations that you can use to process your data. Let's explore some of the most common ones:

Filtering

The filter() operation allows you to select elements from a stream based on a given predicate. For example, to filter out even numbers from a list:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenNumbers = numbers.stream()
                                   .filter(n -> n % 2 == 0)
                                   .collect(Collectors.toList());

System.out.println(evenNumbers); // Output: [2, 4, 6, 8, 10]

Mapping

The map() operation allows you to transform each element of a stream using a provided function. For example, to double the value of each number in a list:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubledNumbers = numbers.stream()
                                      .map(n -> n * 2)
                                      .collect(Collectors.toList());

System.out.println(doubledNumbers); // Output: [2, 4, 6, 8, 10]

Sorting

The sorted() operation allows you to sort the elements of a stream based on their natural order or a custom comparator. For example, to sort a list of strings in reverse alphabetical order:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> sortedNames = names.stream()
                                .sorted(Comparator.reverseOrder())
                                .collect(Collectors.toList());

System.out.println(sortedNames); // Output: [David, Charlie, Bob, Alice]

Reducing

The reduce() operation allows you to combine the elements of a stream into a single value. For example, to calculate the sum of a list of numbers:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                  .reduce(0, Integer::sum);

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

Collecting

The collect() operation allows you to gather the results of a stream pipeline into a new data structure, such as a List, Set, or Map. For example, to collect the unique lengths of a list of strings:

List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
Set<Integer> uniqueLengths = words.stream()
                                  .map(String::length)
                                  .collect(Collectors.toSet());

System.out.println(uniqueLengths); // Output: [5, 6, 4]

These are just a few examples of the many operations and transformations available in the Java Stream API. In the next section, we'll explore some practical examples and use cases for working with streams.

Practical Stream Examples and Use Cases

Now that we've covered the basics of the Java Stream API, let's explore some practical examples and use cases.

Filtering and Transforming Data

Suppose you have a list of Person objects, and you want to find all the adults (age 18 or older) and create a new list of their names in uppercase:

class Person {
    private String name;
    private int age;

    // Constructors, getters, and setters
}

List<Person> people = Arrays.asList(
    new Person("Alice", 25),
    new Person("Bob", 17),
    new Person("Charlie", 32),
    new Person("David", 14)
);

List<String> adultNames = people.stream()
                                .filter(p -> p.getAge() >= 18)
                                .map(Person::getName)
                                .map(String::toUpperCase)
                                .collect(Collectors.toList());

System.out.println(adultNames); // Output: [ALICE, CHARLIE]

Grouping and Summarizing Data

Suppose you have a list of products, and you want to group them by category and calculate the total price for each category:

class Product {
    private String name;
    private String category;
    private double price;

    // Constructors, getters, and setters
}

List<Product> products = Arrays.asList(
    new Product("Apple", "Fruit", 0.5),
    new Product("Banana", "Fruit", 0.25),
    new Product("Carrot", "Vegetable", 0.3),
    new Product("Broccoli", "Vegetable", 0.4)
);

Map<String, Double> totalPriceByCategory = products.stream()
                                                  .collect(Collectors.groupingBy(
                                                      Product::getCategory,
                                                      Collectors.summingDouble(Product::getPrice)
                                                  ));

System.out.println(totalPriceByCategory); // Output: {Fruit=0.75, Vegetable=0.7}

Parallel Processing

Streams can be easily parallelized to take advantage of multi-core processors and improve performance on large datasets. Here's an example of how to use parallel streams to calculate the sum of the first 1 million integers:

long sum = LongStream.rangeClosed(1, 1_000_000)
                     .parallel()
                     .sum();

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

By using the parallel() method, the stream operations are distributed across multiple threads, allowing the computation to be performed more efficiently.

These are just a few examples of the many practical use cases for the Java Stream API. The power of streams lies in their ability to express complex data processing pipelines in a concise and readable manner, while also providing the flexibility to optimize performance and scalability.

Summary

By the end of this tutorial, you will have a solid understanding of the Java Stream API and how to apply it to process collections effectively. You will learn about common stream operations, practical examples, and real-world use cases, empowering you to enhance your Java programming skills and write more expressive, functional, and performant code.

Other Java Tutorials you may like