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;
JavaJava 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:
- Intermediate Operations
- 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);
}
}
JavaOutput
Original list: [1, 2, 3, 4, 5]
Squared list: [1, 4, 9, 16, 25]
Java2. 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);
}
}
JavaOutput
Original list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Even numbers: [2, 4, 6, 8, 10]
JavaIn 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);
}
}
JavaOutput
Original list: [banana, apple, grape, orange, pear]
Sorted list: [apple, banana, grape, orange, pear]
JavaHere 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);
}
}
JavaOutput
Original list with duplicates: [1, 2, 3, 1, 2, 3, 4, 5]
List after removing duplicates: [1, 2, 3, 4, 5]
JavaIn 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);
}
}
JavaOutput
1
2
3
4
5
JavaThis 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);
}
}
JavaOutput
6
7
8
9
10
JavaHere 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);
}
}
JavaOutput
Collected list: [apple, banana, cherry]
Collected set: [1, 2, 3, 4, 5]
Collected map: {5=apple, 6=banana, 6=cherry}
Java2. 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));
}
}
JavaOutput
Apple
Banana
Orange
Mango
Java3. 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(""));
}
}
JavaOutput
Sum of all elements: 15
Maximum element: 5
Concatenated string: Hello world!
JavaConclusion
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
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.
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.
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.