21. Streams and the Stream API in Java
The Stream API in Java is a modern and functional approach to processing sequences of elements. Introduced in Java 8, the Stream API represents a major shift in the way developers can write concise, readable code to manipulate collections of data. In this chapter, we'll explore the power of Streams, from basic concepts to advanced techniques.
Introduction to Streams
Streams are abstractions that allow you to process elements of a collection in a declarative way. Instead of explicitly iterating over a collection using loops, you can use the Stream API to describe what you want to do with these elements. The Stream API supports aggregation operations such as filter, map, reduce and many others.
Creating Streams
You can create a Stream in several ways. One of the most common ways is from collections, calling the stream()
method:
List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream = list.stream();Another way is to use static methods of the
Stream
class to create streams of specific arrays or values:
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5); IntStream intStream = Arrays.stream(new int[] {1, 2, 3, 4, 5});
Intermediate Operations
Intermediate operations are those that transform a Stream into another Stream, such as filter
, map
and sorted
. They are lazy, meaning they are not executed until a terminal operation is invoked.
- filter: Selects elements that meet a specific criteria.
stream.filter(s -> s.startsWith("a"));
- map: Applies a function to each element and returns a Stream with the results.
stream.map(String::toUpperCase);
- sorted: Sorts the elements according to the natural order or a provided Comparator.
stream.sorted(Comparator.naturalOrder());
Terminal Operations
Terminal operations are those that produce a result or side effect, such as forEach
, collect
, reduce
and count
. When a terminal operation is invoked, Stream processing begins.
- forEach: Executes an action for each element of the Stream.
stream.forEach(System.out::println);
- collect: Collects the Stream elements into a collection or other type of result.
List<String> collected = stream.collect(Collectors.toList());
- reduce: Combines Stream elements to produce a single value.
Optional<String> concatenated = stream.reduce(String::concat);
- count: Returns the number of elements in the Stream.
long count = stream.count();
Parallel Streams
Parallel streams allow operations to be executed in parallel on different threads, which can lead to better performance on multiprocessor systems. To create a parallel Stream, you can call the parallelStream()
method on a collection or the parallel()
method on an existing Stream.
Stream<String> parallelStream = list.parallelStream();
Performance Considerations
When using Streams, it is important to be aware of the performance implications. Operations such as sorted
and distinct
can be costly as they require additional processing. Furthermore, the use of Parallel Streams does not always guarantee a performance increase and may even be counterproductive if the cost of splitting and merging tasks is greater than the gain of parallel processing.
Good Practices
When working with Streams, it is essential to follow some good practices to keep the code clean and efficient:
- Use reference methods whenever possible to improve readability.
- Avoid changing the state of external objects within Stream operations.
- Prefer stateless operations such as
map
andfilter
over stateful operations such assorted
. - Consider code readability and maintainability over micro-optimizations.
Conclusion
The Stream API in Java offers a powerful and flexible way to work with collections of data. With a wide range of intermediate and terminal operations, Streams allow developers to write more expressive and concise code. By correctly understanding and applying Streams concepts, you can significantly improve the quality and performance of your Java code.