Java Stream API Functional Details
The Java Stream API is a new feature introduced in Java 8 that provides a more concise, efficient, and user-friendly way to operate on collections and arrays. The Stream API allows us to process data declaratively rather than imperatively through loops and conditional statements.
The Stream API offers a streaming approach where operations can be performed on elements within a collection or array, such as filtering, mapping, sorting, and aggregation. These operations are constructed using a series of intermediate steps and conclude with terminal operations. Intermediate operations generate new Stream objects, while terminal operations trigger the traversal execution of the stream object to produce a final result or side effects.
The main advantages of the Stream API include:
- Conciseness: It provides a more concise, efficient, and user-friendly way to operate on collections and arrays compared to traditional imperative loops.
- Parallel Processing: The Stream API supports parallel processing, allowing full utilization of hardware resources across multi-core CPUs to improve efficiency.
- Lazy Evaluation: Operations are evaluated lazily; computation only occurs when a result is needed, which helps save computational resources.
- Composability: Intermediate operations can be chained together to form an operation pipeline that can be dynamically constructed based on requirements.
- Rich Set of Operations: It supports numerous common operations including filtering, mapping, sorting, and aggregation, catering to various data processing needs.
The Java Stream API primarily covers the following aspects:
- Creating Streams: Streams can be created via methods like
Collection.stream(),Arrays.stream(), orStream.of()on a list of elements. - Intermediate Operations: These operations transform, filter, sort streams without triggering execution immediately. Common examples include
map(),filter(),sorted(), anddistinct(). - Terminal Operations: These finalize the stream by producing results or side effects. Examples include
forEach(),count(),reduce(), andcollect(). - Parallel Processing: The API facilitates parallel processing easily, enhancing program efficiency via
parallelStream()for conversion to parallel streams andsequentialStream()for converting back to sequential ones.
Using the Stream API helps developers handle data more conveniently, reducing boilerplate code while improving program efficiency and readability. However, it is important to note that this API does not fit every scenario; selection should be made based on specific contexts.
Simple Example
The following example demonstrates how to use the Stream API to filter, map, and aggregate an integer collection:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.stream()
.filter(n -> n % 2 == 0) // Keep only even numbers
.mapToInt(Integer::intValue) // Map to int primitive type (optional here as stream is already of Integers but good practice for primitives)
.sum();
System.out.println(sum); // Output: 30
In this example, we first create a collection containing ten integers. Then, the stream() method retrieves a Stream object from the list. The filter() method filters out elements, keeping only even numbers. Subsequently, mapToInt() maps these remaining values to an int primitive type (though strictly speaking, boxing/unboxing happens implicitly here). Finally, sum() aggregates the mapped integers to produce the final result of 30.
Creating Streams
Methods for creating a Stream in Java's Stream API include:
Creating from Collections or Arrays
List<String> list = Arrays.asList("a", "b", "c"); Stream<String> streamFromList = list.stream(); String[] array = {"a", "b", "c"}; Stream<String> streamFromArray = Arrays.stream(array);Creating from a Set of Elements
Stream<String> streamOfElements = Stream.of("a", "b", "c");Using
Stream.generate(): Accepts aSupplier<T>parameter to generate elements for the stream lazily.Stream<String> streamGenerated = Stream.generate(() -> "element").limit(10);Using
Stream.iterate(): Accepts an initial value and aUnaryOperator<T>parameter to generate subsequent elements based on the previous one.Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20);Using
Files.lines(): Accepts a file path and returns a Stream of lines from that file.Path path = Paths.get("file.txt"); Stream<String> streamOfFileLines = Files.lines(path);Using
Pattern.splitAsStream(): Accepts a string and a regex pattern to return a Stream containing matching parts found in the input string.String str = "a-b-c"; Stream<String> streamOfStrings = Pattern.compile("-").splitAsStream(str);
Intermediate Operations
Definition: Operations that do not trigger execution immediately, are used to build pipelines, and return a Stream object itself. These operations include: distinct, sorted, skip, limit, peek, mapToLong, mapToInt, mapToDouble, and flatMap.
Terminal Operations
Definition: Operations that trigger actual execution and produce concrete results or side effects. Examples include: collect, max, min, count, allMatch, anyMatch, noneMatch, findFirst, findAny, forEach, toArray, and reduce.
Collect
In Java 8, the Stream API provides a collect() method to gather elements from a stream into containers (like Lists, Sets, Maps) while supporting grouping, partitioning, and aggregation. The collect() method accepts a Collector object as an argument to specify how collection should be performed.
The basic usage of collect() involves gathering Stream elements into standard container types like List or Set. For instance, the following example demonstrates collecting stream elements into a List:
List<Integer> list = Stream.of(1, 2, 3, 4, 5)
.collect(Collectors.toList());
System.out.println(list); // Output: [1, 2, 3, 4, 5]
In this example, Collectors.toList() returns a Collector object that gathers elements into a List. Beyond Lists, the collect() method can also populate Sets and Maps, or perform complex grouping and partitioning logic. The following example illustrates collecting stream elements into a Map:
Map<Integer, String> map = Stream.of("apple", "banana", "orange")
.collect(Collectors.toMap(String::length, Function.identity()));
System.out.println(map); // Output: {5=apple, 6=orange, 6=banana}
Here, Collectors.toMap() creates a Collector that builds the Map. The key is derived from the string length (String::length), and the value is the original string itself (Function.identity()). Since two strings ("banana" and "orange") have the same length (6), they are merged under the single integer key 6.
In addition to toList() and toMap(), the Collectors class offers numerous other methods for grouping, partitioning, reducing, etc., allowing selection based on specific requirements.
Parallel Streams
The Stream API introduced in Java 8 provides a convenient way to handle collections via streaming processing. While it supports sequential data processing, it also enables parallel processing to leverage the performance benefits of multi-core processors.
Within the Stream API, the parallel() method can be used to convert a sequential stream into a parallel one. A parallel stream executes concurrently across multiple threads, significantly speeding up processing for large datasets. For example:
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add(i);
}
Stream<Integer> parallelStream = list.parallelStream();
parallelStream.forEach(System.out::println);
In the code above, parallelStream() converts the List into a Parallel Stream. The subsequent call to forEach() iterates over all elements and prints them. During parallel execution, the Stream API automatically partitions data into chunks and distributes these tasks across different threads for processing.
It is important to note that parallel processing does not always outperform sequential processing. Parallelism introduces overhead related to thread coordination and communication, which can sometimes slow down performance compared to a simple single-threaded loop. Therefore, it is recommended to test and optimize based on the specific workload before relying solely on parallel streams for maximum speed.
Sequential Streams
A Sequential Stream in the Stream API represents a type of stream that processes elements one by one according to their order in the source data. In sequential processing, operations are executed within a single thread, ensuring that the order of element processing matches the original order from the data source.
To obtain a sequential stream, you can invoke the sequential() method on any Stream interface:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> sequentialStream = numbers.stream().sequential();
In this example, we first create a list of integers using Arrays.asList(), then obtain an ordinary stream via the stream() method, and finally convert it to a Sequential Stream using sequential().
Sequential streams support various operations such as filtering, mapping, sorting, etc., which can be chained together. After these transformations are complete, elements within the stream can be processed individually using the forEach() method:
sequentialStream.filter(n -> n % 2 == 0)
.map(n -> n * n)
.sorted()
.forEach(System.out::println);
In this example, we filter elements to keep only even numbers (n % 2 == 0), map them to their squares (n * n), sort the resulting sequence, and finally print each element using forEach() to the console.
It is crucial to remember that sequential processing operates within a single thread; consequently, handling large volumes of data may encounter performance bottlenecks due to lack of parallelism. If parallel execution is required for better throughput on large datasets, Parallel Streams should be utilized instead.