Home » Stream API in Java 8

Stream API in Java 8

Stream API in Java 8

Introduction

The Stream API in Java, introduced in Java 8, is a powerful and functional way to process collections of objects. It allows for concise and declarative operations on sequences of elements, such as arrays or collections. It provides a declarative approach, allowing you to focus on what you want to achieve with the data rather than how to iterate through it manually using loops. We’ll delve into the transformative power of the Java Stream API, unraveling its capabilities, use-cases, and nuances that every Java developer should be cognizant of.

The Java Stream API, introduced in Java 8, represents one of the most transformative additions to the Java language and its core libraries. It’s not just a set of new methods or utilities, but rather a paradigm shift that encourages developers to embrace a functional approach in handling data. Before we proceed, it’s essential to understand why such an API was necessary and how it fundamentally changed the way Java developers operate on collections.

Stream API is used to process collections of objects. A stream in Java is a sequence of objects that supports various methods which can be pipelined to produce the desired result.

How to Create Java Stream ?

Java Stream Creation is one of the most basic steps before considering the functionalities of the Java Stream. Below is the syntax given on how to declare Java Stream.

Stream<T> stream;
Java

Java Stream Features

  • Sequence Processing: Streams represent sequences of elements that can be processed sequentially or in parallel.
  • Functional Operations: Streams support functional-style operations such as mapping, filtering, reducing, and iterating over elements.
  • Lazy Evaluation: Intermediate operations on streams are lazily evaluated, meaning they are only executed when a terminal operation is invoked. This improves efficiency by avoiding unnecessary computation.
  • Pipeline: Streams support chaining of operations into a pipeline, where the output of one operation becomes the input for the next.
  • Immutable Data: Streams do not modify the underlying data source. Instead, they produce a new stream with the transformed elements, leaving the original data unchanged.
  • Parallel Processing: Streams can be processed in parallel using parallel streams, which automatically distribute the workload across multiple threads for improved performance.
  • Source Flexibility: Streams can be created from various data sources including collections, arrays, I/O resources, and generator functions.
  • Terminal Operations: Terminal operations mark the end of a stream pipeline and produce a result or a side-effect. Common terminal operations include forEach(), collect(), reduce(), count(), and anyMatch().
  • Intermediate Operations: Intermediate operations modify, filter, or transform the elements of a stream, producing a new stream as output. Examples include map(), filter(), flatMap(), distinct(), sorted(), and limit().
  • Reduction Operations: Reduction operations combine elements of a stream into a single result. Examples include reduce() for general-purpose reduction, sum(), min(), max(), and average() for specific reduction tasks.
  • Primitive Type Streams: Specialized interfaces such as IntStream, LongStream, DoubleStream are available for working with primitive data types, providing better performance and readability.
  • Optional: Streams support Optional<T> for handling cases where a value may be absent. This helps avoid null pointer exceptions and provides a clearer way to handle optional values.
  • Parallel Stream Considerations: While parallel streams can offer performance improvements, they require careful consideration due to potential concurrency issues and overhead. It’s important to evaluate whether parallel processing is suitable for a given task and to ensure thread safety when working with shared resources.

Different Operations On Streams

There are two types of Operations in Streams:

  1. Intermediate Operations
  2. Terminate Operations

Intermediate Operations

Intermediate operations in Java Streams are operations that transform or filter the elements of a stream, producing a new stream as output. These operations do not trigger execution immediately; instead, they form a pipeline that is executed when a terminal operation is invoked

Important Intermediate Operations in Java Stream API

There are a few Intermediate Operations mentioned below:

1. map()

The map method is used to transforming each element in the stream. This operation is particularly useful when you want to convert elements from one type to another or modify their state.

Example

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

public class MapExample {
    public static void main(String[] args) {
        // Create a list of integers
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // Using map() to square each element of the list
        List<Integer> squaredNumbers = numbers.stream()
                                              .map(num -> num * num)
                                              .collect(Collectors.toList());

        // Print the squared numbers
        System.out.println("Original list: " + numbers);
        System.out.println("Squared list: " + squaredNumbers);
    }
}
Java

Output

Original list: [1, 2, 3, 4, 5]
Squared list: [1, 4, 9, 16, 25]
Java

2. filter()

In Java, the filter() method is used in conjunction with streams to select elements based on a given predicate. It filters out elements from a stream that don’t match the specified condition.

Example

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

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

        // Filter out even numbers
        List<Integer> evenNumbers = numbers.stream()
                                           .filter(num -> num % 2 == 0)
                                           .collect(Collectors.toList());

        // Print the even numbers
        System.out.println("Original list: " + numbers);
        System.out.println("Even numbers: " + evenNumbers);
    }
}
Java

Output

Original list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Even numbers: [2, 4, 6, 8, 10]
Java

In this example, the filter() method is used to select only the even numbers from the list. The lambda expression num -> num % 2 == 0 serves as the predicate, returning true for even numbers and false for odd numbers. Finally, the collect(Collectors.toList()) method collects the filtered elements into a new list.

3. sorted()

In Java, the sorted() method is used to sort the elements of a stream in natural order or using a custom comparator. It returns a new stream with the elements sorted according to their natural order or the specified comparator.

Example


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

public class SortedExample {
    public static void main(String[] args) {
        // Create a list of strings
        List<String> words = Arrays.asList("banana", "apple", "grape", "orange", "pear");

        // Sort the words in natural order
        List<String> sortedWords = words.stream()
                                       .sorted()
                                       .collect(Collectors.toList());

        // Print the sorted words
        System.out.println("Original list: " + words);
        System.out.println("Sorted list: " + sortedWords);
    }
}

Java

Output

Original list: [banana, apple, grape, orange, pear]
Sorted list: [apple, banana, grape, orange, pear]
Java

Here how the sorted() method can be used in Java to sort elements of a stream in either natural order or based on a custom comparator.

4. distinct()

The distinct() method is used to remove duplicate elements from a stream. Example

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

public class DistinctExample {
    public static void main(String[] args) {
        // Create a list of integers with duplicates
        List<Integer> numbersWithDuplicates = Arrays.asList(1, 2, 3, 1, 2, 3, 4, 5);

        // Use distinct() to remove duplicates
        List<Integer> distinctNumbers = numbersWithDuplicates.stream()
                                                             .distinct()
                                                             .collect(Collectors.toList());

        // Print the distinct numbers
        System.out.println("Original list with duplicates: " + numbersWithDuplicates);
        System.out.println("List after removing duplicates: " + distinctNumbers);
    }
}
Java

Output

Original list with duplicates: [1, 2, 3, 1, 2, 3, 4, 5]
List after removing duplicates: [1, 2, 3, 4, 5]
Java

In this example, the distinct() method is applied to the stream of integers to remove duplicate elements. The resulting stream contains only unique elements, which are then collected back into a list.

5.limit()

In Java, the limit() method is used in streams to limit the number of elements that are processed by subsequent operations. It returns a stream consisting of the elements up to the specified maximum size. Example


import java.util.stream.Stream;

public class LimitExample {
    public static void main(String[] args) {
        // Create a stream of integers from 1 to 10
        Stream<Integer> numbers = Stream.iterate(1, n -> n + 1).limit(10);

        // Print the first 5 elements
        numbers.limit(5).forEach(System.out::println);
    }
}
Java

Output

1
2
3
4
5
Java

This demonstrates how the limit() method can be used to restrict the number of elements in a stream, allowing you to efficiently process large datasets or control the size of the data being operated on.

6.skip()

In Java, the skip() method is used in streams to skip a specified number of elements from the beginning of the stream. It returns a stream that contains the remaining elements after skipping the specified number of elements.

Example

import java.util.stream.Stream;

public class SkipExample {
    public static void main(String[] args) {
        // Create a stream of integers from 1 to 10
        Stream<Integer> numbers = Stream.iterate(1, n -> n + 1).limit(10);

        // Skip the first 5 elements
        Stream<Integer> skippedNumbers = numbers.skip(5);

        // Print the remaining elements
        skippedNumbers.forEach(System.out::println);
    }
}
Java

Output

6
7
8
9
10
Java

Here how the skip() method can be used to ignore a specified number of elements at the beginning of a stream, allowing you to process or manipulate the remaining elements as needed.

Terminal Operations for Java Stream API

In Java, terminal operations in streams are operations that produce a result or a side-effect and terminate the processing of the stream. Moreover, unlike intermediate operations, terminal operations trigger the execution of the stream pipeline and consume the stream. Consequently, once a terminal operation is invoked on a stream, no further intermediate operations can be applied to that stream.

Important Terminal Operations for Java Stream API

There are a few Terminal Operations mentioned below:

1. collect()

In Java, the collect() method is a terminal operation in streams that allows you to accumulate the elements of a stream into a collection, such as a List, Set, or Map, or perform a custom reduction operation. It is one of the most versatile terminal operations and is commonly used to gather elements from a stream into a container.

Example

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

public class CollectExample {
    public static void main(String[] args) {
        // Create a stream of strings
        Stream<String> stream = Stream.of("apple", "banana", "cherry");

        // Collect the elements into a list
        List<String> collectedList = stream.collect(Collectors.toList());
        System.out.println("Collected list: " + collectedList);

        // Create another stream
        Stream<Integer> intStream = Stream.of(1, 2, 3, 4, 5);

        // Collect the elements into a set
        Set<Integer> collectedSet = intStream.collect(Collectors.toSet());
        System.out.println("Collected set: " + collectedSet);

        // Create one more stream
        Stream<String> fruitStream = Stream.of("apple", "banana", "cherry");

        // Collect the elements into a map (using the length of the string as the key)
        Map<Integer, String> collectedMap = fruitStream.collect(Collectors.toMap(
                String::length,  // Key mapper function
                s -> s,         // Value mapper function
                (existing, replacement) -> existing)); // Merge function (in case of duplicate keys)
        System.out.println("Collected map: " + collectedMap);
    }
}
Java

Output

Collected list: [apple, banana, cherry]
Collected set: [1, 2, 3, 4, 5]
Collected map: {5=apple, 6=banana, 6=cherry}
Java

2. forEach()

In Java, the forEach() method is a terminal operation in streams that allows you to perform an action for each element of the stream. It accepts a Consumer functional interface, which represents an operation that accepts a single input argument and returns no result. This method is useful when you want to perform an action, such as printing each element, on each element of the stream.

Example

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

public class ForEachExample {
    public static void main(String[] args) {
        // Create a list of strings
        List<String> fruits = Arrays.asList("Apple", "Banana", "Orange", "Mango");

        // Print each element of the list using forEach() method
        fruits.stream().forEach(fruit -> System.out.println(fruit));
    }
}
Java

Output

Apple
Banana
Orange
Mango
Java

3. reduce()

In Java, the reduce() method in streams is a terminal operation that performs a reduction on the elements of the stream. It combines the elements of the stream into a single result using a binary operator. The reduce() method is useful for tasks such as calculating the sum, finding the maximum or minimum element, or performing custom aggregations

Example

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

public class ReduceExample {
    public static void main(String[] args) {
        // Create a list of integers
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // Calculate the sum of all elements using reduce()
        Optional<Integer> sum = numbers.stream().reduce((x, y) -> x + y);
        System.out.println("Sum of all elements: " + sum.orElse(0));

        // Find the maximum element using reduce()
        Optional<Integer> max = numbers.stream().reduce(Integer::max);
        System.out.println("Maximum element: " + max.orElse(0));

        // Concatenate all strings in a list using reduce()
        List<String> strings = Arrays.asList("Hello", " ", "world", "!");
        Optional<String> concatenatedString = strings.stream().reduce((x, y) -> x + y);
        System.out.println("Concatenated string: " + concatenatedString.orElse(""));
    }
}
Java

Output

Sum of all elements: 15
Maximum element: 5
Concatenated string: Hello world!
Java

Conclusion

Java streams offer a powerful and concise way to process collections of data. They leverage functional programming concepts, lazy evaluation, and a fluent API to enable developers to express complex data manipulation tasks in a clear and succinct manner. By promoting immutability, streams encourage writing code that is easier to understand, maintain, and parallelize.

Java streams provide a variety of intermediate and terminal operations, allowing for flexible and efficient data processing. From mapping and filtering to reduction and aggregation, streams offer a wide range of capabilities for transforming, filtering, and extracting data from collections.

Moreover, Java streams promote parallelism, efficiently utilizing multi-core processors to improve performance on large datasets. However, it is important to use parallel processing judiciously. Factors such as data size, computational complexity, and potential overhead must be considered.

Overall, Java streams have become an integral part of modern Java programming, offering a concise and expressive way to handle data processing tasks. With their rich set of features and functional programming paradigm, streams enhance code readability, maintainability, and scalability, making them a valuable tool for Java developers.

Frequently Asked Questions

1. What are Java Streams?

Java Streams are a sequence of elements supporting sequential and parallel aggregate operations. They enable you to process collections of objects in a functional style, leveraging functional programming techniques.

2. What are the benefits of using Streams in Java?

Streams provide several benefits including concise and expressive code, support for parallel processing, lazy evaluation, and easy composition of operations like filtering, mapping, and reduction.

3. What is the difference between Intermediate and Terminal operations in Streams?

Intermediate operations, like map() or filter(), transform or filter the elements of a stream and return a new stream. Furthermore, terminal operations, such as forEach() or collect(), produce a result or side-effect. Consequently, after their execution, the stream is considered consumed.