Stream API in Java 8
Introduction
Java 8 introduced the Stream API, a powerful tool for processing sequences of elements in a functional style. This API simplifies many common operations on collections, such as filtering, mapping, and reducing, making code more readable and expressive. In this article, we will explore the Stream API in depth, covering its basic concepts, intermediate and terminal operations, parallel streams, and practical examples.
What is a Stream?
A stream is a sequence of elements that supports various operations to process those elements. Unlike collections, streams are not data structures; they do not store elements. Instead, they convey elements from a source (such as a collection, array, or I/O channel) through a pipeline of computational operations.
Creating Streams
Streams can be created from various data sources. Here are some common ways to create streams:
From Collections:
1 2
List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream = list.stream();
From Arrays:
1 2
String[] array = {"a", "b", "c"}; Stream<String> stream = Arrays.stream(array);
From Values:
1
Stream<String> stream = Stream.of("a", "b", "c");
From Files:
1 2 3 4 5
try (Stream<String> stream = Files.lines(Paths.get("file.txt"))) { stream.forEach(System.out::println); } catch (IOException e) { e.printStackTrace(); }
Stream Operations
Stream operations are divided into intermediate and terminal operations.
Intermediate Operations
Intermediate operations return a new stream, allowing multiple operations to be chained together. These operations are lazy; they are not executed until a terminal operation is invoked.
filter:
Filters elements based on a predicate.1
Stream<String> stream = list.stream().filter(s -> s.startsWith("a"));
map:
Transforms each element using a provided function.1
Stream<String> stream = list.stream().map(String::toUpperCase);
sorted:
Sorts the elements.1
Stream<String> stream = list.stream().sorted();
distinct:
Removes duplicate elements.1
Stream<String> stream = list.stream().distinct();
limit:
Truncates the stream to the first n elements.1
Stream<String> stream = list.stream().limit(2);
skip:
Skips the first n elements.1
Stream<String> stream = list.stream().skip(2);
Terminal Operations
Terminal operations produce a result or a side-effect and mark the end of the stream. After a terminal operation is executed, the stream is considered consumed and cannot be used further.
forEach:
Performs an action for each element.1
list.stream().forEach(System.out::println);
collect:
Transforms the stream into a collection or another data structure.1
List<String> result = list.stream().filter(s -> s.startsWith("a")).collect(Collectors.toList());
reduce:
Combines the elements into a single result.1
Optional<String> concatenated = list.stream().reduce((s1, s2) -> s1 + s2);
count:
Returns the number of elements in the stream.1
long count = list.stream().count();
anyMatch:
Returns true if any elements match the provided predicate.1
boolean anyStartsWithA = list.stream().anyMatch(s -> s.startsWith("a"));
allMatch:
Returns true if all elements match the provided predicate.1
boolean allStartWithA = list.stream().allMatch(s -> s.startsWith("a"));
noneMatch:
Returns true if no elements match the provided predicate.1
boolean noneStartWithZ = list.stream().noneMatch(s -> s.startsWith("z"));
findFirst:
Returns the first element in the stream, if any.1
Optional<String> first = list.stream().findFirst();
findAny:
Returns any element in the stream, if any.1
Optional<String> any = list.stream().findAny();
Advanced Stream Operations
FlatMap
The flatMap method is used to flatten nested streams. For example, if you have a list of lists and want to create a single list of all elements, you can use flatMap.
1
2
3
4
5
6
7
8
List<List<String>> listOfLists = Arrays.asList(
Arrays.asList("a", "b", "c"),
Arrays.asList("d", "e", "f")
);
List<String> flatList = listOfLists.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
Grouping and Partitioning
Streams provide powerful ways to group and partition data using the Collectors
utility class.
Grouping By:
Groups elements by a classifier function.1 2
Map<Integer, List<String>> groupedByLength = list.stream() .collect(Collectors.groupingBy(String::length));
Partitioning By:
Partitions elements into two groups based on a predicate.1 2
Map<Boolean, List<String>> partitionedByA = list.stream() .collect(Collectors.partitioningBy(s -> s.startsWith("a")));
Parallel Streams
Java 8 streams can be executed in parallel to leverage multi-core processors.
This is done using the parallelStream
method or by calling parallel
on a stream.
1
2
3
4
5
List<String> list = Arrays.asList("a", "b", "c", "d", "e", "f");
List<String> result = list.parallelStream()
.filter(s -> s.startsWith("a"))
.collect(Collectors.toList());
Parallel streams can significantly improve performance for large datasets, but they come with some overhead. It’s important to measure and test performance to ensure that parallel processing is beneficial for your specific use case.
Practical Examples
Example 1: Filtering and Collecting
Suppose you have a list of integers and you want to filter out the even numbers and collect the result into a new list.
1
2
3
4
5
6
7
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]
Example 2: Mapping and Reducing
Consider a list of strings representing numbers. You want to convert them to integers and find their sum.
1
2
3
4
5
6
7
List<String> numberStrings = Arrays.asList("1", "2", "3", "4", "5");
int sum = numberStrings.stream()
.map(Integer::parseInt)
.reduce(0, Integer::sum);
System.out.println(sum); // Output: 15
Example 3: Grouping By
Given a list of strings, group them by their length.
1
2
3
4
5
6
7
List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
Map<Integer, List<String>> groupedByLength = words.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(groupedByLength);
// Output: {5=[apple], 6=[banana, cherry], 4=[date], 10=[elderberry]}
Example 4: Partitioning By
Partition a list of strings into those that start with ‘a’ and those that do not.
1
2
3
4
5
6
7
List<String> words = Arrays.asList("apple", "banana", "avocado", "cherry");
Map<Boolean, List<String>> partitionedByA = words.stream()
.collect(Collectors.partitioningBy(s -> s.startsWith("a")));
System.out.println(partitionedByA);
// Output: {false=[banana, cherry], true=[apple, avocado]}
Conclusion
The Stream API in Java 8 is a powerful tool for processing collections in a functional style. It provides a clear and concise way to perform operations such as filtering, mapping, and reducing. By leveraging streams, you can write more readable, maintainable, and efficient code. However, it is important to understand the differences between sequential and parallel streams and to measure performance to ensure that you are getting the desired benefits. Mastering the Stream API will greatly enhance your Java programming skills and enable you to tackle complex data processing tasks with ease.