16.5 Intermediate Stream Operations
A stream pipeline is composed of stream operations. The stream operations process the elements of the stream to produce some result. After the creation of the initial stream, the elements of the stream are processed by zero or more intermediate operations before the mandatory terminal operation reduces the elements to some final result. The initial stream can undergo several stream transformations (technically called mappings) by the intermediate operations as the elements are processed through the pipeline.
Intermediate operations map a stream to a new stream, and the terminal operation reduces the final stream to some result. Because of the nature of the task they perform, the operations in a stream pipeline are also called map-reduce transformations.
Aspects of Streams, Revisited
We now take a closer look at the following aspects pertaining to streams:
Stream mapping
Lazy execution
Short-circuit evaluation
Stateless and stateful operations
Order of intermediate operations
Non-interfering and stateless behavioral parameters of stream operations
Table 16.3, p. 938, summarizes certain aspects of each intermediate operation. Table 16.4, p. 939, summarizes the intermediate operations provided by the Stream API.
Stream Mapping
Each intermediate operation returns a new stream—that is, it maps the elements of its input stream to an output stream. Intermediate operations can thus be easily recognized. Having a clear idea of the type of the new stream an intermediate operation should produce aids in customizing the operation with an appropriate implementation of its behavioral parameters. Typically, these behavioral parameters are functional interfaces.
Because intermediate operations return a new stream, calls to methods of intermediate operations can be chained, so much so that code written in this method chaining style has become a distinct hallmark of expressing queries with streams.
In Example 16.3, the stream pipeline represents the query to create a list with titles of pop music CDs in a given list of CDs. Stream mapping is illustrated at (1). The initial stream of CDs (Stream<CD>) is first transformed by an intermediate operation (filter()) to yield a new stream that has only pop music CDs (Stream<CD>), and this stream is then transformed to a stream of CD titles (Stream<String>) by a second intermediate operation (map(), p. 921). The stream of CD titles is reduced to the desired result (List<CD>) by the terminal operation (collect()).
In summary, the type of the output stream returned by an intermediate operation need not be the same as the type of its input stream.
Example 16.3 Stream Mapping and Loop Fusion
import java.util.List; public class StreamOps { public static void main(String[] args) { // Query: Create a list with titles of pop music CDs. // (1) Stream Mapping: List<CD> cdList1 = CD.cdList; List<String> popCDs1 = cdList1 .stream() // Initial stream: Stream<CD> .filter(CD::isPop) // Intermediate operation: Stream<CD> .map(CD::title) // Intermediate operation: Stream<String> .toList(); // Terminal operation: List<String> System.out.println("Pop music CDs: " + popCDs1); System.out.println(); // (2) Lazy Evaluation: List<CD> cdList2 = CD.cdList; List<String> popCDs2 = cdList2 .stream() // Initial stream: Stream<CD> .filter(cd -> { // Intermediate operation: Stream<CD> System.out.println("Filtering: " + cd // (3) + (cd.isPop() ? " is pop CD." : " is not pop CD.")); return cd.isPop(); }) .map(cd -> { // Intermediate operation: Stream<String> System.out.println("Mapping: " + cd.title()); // (4) return cd.title(); }) .toList(); // Terminal operation: List<String> System.out.println("Pop music CDs: " + popCDs2); } }
Output from the program:
Pop music CDs: [Java Jive, Lambda Dancing] Filtering: <Jaav, "Java Jive", 8, 2017, POP> is pop CD. Mapping: Java Jive Filtering: <Jaav, "Java Jam", 6, 2017, JAZZ> is not pop CD. Filtering: <Funkies, "Lambda Dancing", 10, 2018, POP> is pop CD. Mapping: Lambda Dancing Filtering: <Genericos, "Keep on Erasing", 8, 2018, JAZZ> is not pop CD. Filtering: <Genericos, "Hot Generics", 10, 2018, JAZZ> is not pop CD. Pop music CDs: [Java Jive, Lambda Dancing]
Lazy Execution
A stream pipeline does not execute until a terminal operation is invoked. In other words, its intermediate operations do not start processing until their results are needed by the terminal operation. Intermediate operations are thus lazy, in contrast to the terminal operation, which is eager and executes when it is invoked.
An intermediate operation is not performed on all elements of the stream before performing the next operation on all elements resulting from the previous stream. Rather, the intermediate operations are performed back-to-back on each element in the stream. In a sense, the loops necessary to perform each intermediate operation on all elements successively are fused into a single loop (technically called loop fusion). Thus only a single pass is required over the elements of the stream.
Example 16.3 illustrates loop fusion resulting from lazy execution of a stream pipeline at (2). The intermediate operations now include print statements to announce their actions at (3) and (4). Note that we do not advocate this practice for production code. The output shows that the elements are processed one at a time through the pipeline when the terminal operation is executed. A CD is filtered first, and if it is a pop music CD, it is mapped to its title and the terminal operation includes this title in the result list. Otherwise, the CD is discarded. When there are no more CDs in the stream, the terminal operation completes, and the stream is consumed.
Short-circuit Evaluation
The lazy nature of streams allows certain kinds of optimizations to be performed on stream operations. We have already seen an example of such an optimization that results in loop fusion of intermediate operations.
In some cases, it is not necessary to process all elements of the stream in order to produce a result (technically called short-circuit execution). For instance, the limit() intermediate operation creates a stream of a specified size, making it unnecessary to process the rest of the stream once this limit is reached. A typical example of its usage is to turn an infinite stream into a finite stream. Another example is the takeWhile() intermediate operation that short-circuits stream processing once its predicate becomes false.
Certain terminal operations (anyMatch(), allMatch(), noneMatch(), findFirst(), findAny()) are also short-circuit operations, since they do not need to process all elements of the stream in order to produce a result (p. 949).
Stateless and Stateful Operations
An stateless operation is one that can be performed on a stream element without taking into consideration the outcome of any processing done on previous elements or on any elements yet to be processed. In other words, the operation does not retain any state from processing of previous elements in order to process a new element. Rather, the operation can be performed on an element independently of how the other elements are processed.
A stateful operation is one that needs to retain state from previously processed elements in order to process a new element.
The intermediate operations distinct(), dropWhile(), limit(), skip(), sorted(), and takeWhile() are stateful operations. All other intermediate operations are stateless. Examples of stateless intermediate operations include the filter() and map() operations.
Order of Intermediate Operations
The order of intermediate operations in a stream pipeline can impact the performance of a stream pipeline. If intermediate operations that reduce the size of the stream can be performed earlier in the pipeline, fewer elements need to be processed by the subsequent operations.
Moving intermediate operations such as filter(), distinct(), dropWhile(), limit(), skip(), and takeWhile() earlier in the pipeline can be beneficial, as they all decrease the size of the input stream. Example 16.4 implements two stream pipelines at (1) and (2) to create a list of CD titles, but skipping the first three CDs. The map() operation transforms each CD to its title, resulting in an output stream with element type String. The example shows how the number of elements processed by the map() operation can be reduced if the skip() operation is performed before the map() operation (p. 921).
Example 16.4 Order of Intermediate Operations
import java.util.List; public final class OrderOfOperations { public static void main(String[] args) { List<CD> cdList = CD.cdList; // Map before skip. List<String> cdTitles1 = cdList .stream() // (1) .map(cd -> { // Map applied to all elements. System.out.println("Mapping: " + cd.title()); return cd.title(); }) .skip(3) // Skip afterwards. .toList(); System.out.println(cdTitles1); System.out.println(); // Skip before map preferable. List<String> cdTitles2 = cdList .stream() // (2) .skip(3) // Skip first. .map(cd -> { // Map not applied to the first 3 elements. System.out.println("Mapping: " + cd.title()); return cd.title(); }) .toList(); System.out.println(cdTitles2); } }
Output from the program:
Mapping: Java Jive Mapping: Java Jam Mapping: Lambda Dancing Mapping: Keep on Erasing Mapping: Hot Generics [Keep on Erasing, Hot Generics] Mapping: Keep on Erasing Mapping: Hot Generics [Keep on Erasing, Hot Generics]
Non-interfering and Stateless Behavioral Parameters
One of the main goals of the Stream API is that the code for a stream pipeline should execute and produce the same results whether the stream elements are processed sequentially or in parallel. In order to achieve this goal, certain constraints are placed on the behavioral parameters—that is, on the lambda expressions and method references that are implementations of the functional interface parameters in stream operations. These behavioral parameters, as the name implies, allow the behavior of a stream operation to be customized. For example, the predicate supplied to the filter() operation defines the criteria for filtering the elements.
Most stream operations require that their behavioral parameters are non-interfering and stateless. A non-interfering behavioral parameter does not change the stream data source during the execution of the pipeline, as this might not produce deterministic results. The exception to this is when the data source is concurrent, which guarantees that the source is thread-safe. A stateless behavioral parameter does not access any state that can change during the execution of the pipeline, as this might not be thread-safe.
If the constraints are violated, all bets are off, resulting in incorrect results being computed, which causes the stream pipeline to fail. In addition to these constraints, care should be taken to introduce side effects via behavioral parameters, as these might introduce other concurrency-related problems during parallel execution of the pipeline.
The aspects of intermediate operations mentioned in this subsection will become clearer as we fill in the details in subsequent sections.
Filtering
Filters are stream operations that select elements based on some criteria, usually specified as a predicate. This section discusses different ways of filtering elements, selecting unique elements, skipping elements at the head of a stream, and truncating a stream.
The following methods are defined in the Stream<T> interface, and analogous methods are also defined in the IntStream, LongStream, and DoubleStream interfaces:
Filtering Using a Predicate
We have already seen many examples of filtering stream elements in this chapter. The first example of using the Stream.filter() method was presented in Figure 16.1, p. 885.
Filtering a collection using the Iterator.remove() method and the Collection.removeIf() method is discussed in §13.3, p. 691, and §15.2, p. 796, respectively.
The filter() method can be used on both object and numeric streams. The Stream.filter() method accepts a Predicate<T> as an argument. The predicate is typically implemented as a lambda expression or a method reference defining the selection criteria. It yields a stream consisting of elements from the input stream that satisfy the predicate. The elements that do not match the predicate are discarded.
In Figure 16.3, Query 1 selects those CDs from a list of CDs (CD.cdList) whose titles are in a set of popular CD titles (popularTitles). The Collection.contains() method is used in the predicate to determine if the title of a CD is in the set of popular CD titles. The execution of the stream pipeline shows there are only two such CDs (cd0, cd1). CDs that do not satisfy the predicate are discarded.
We can express the same query using the Collection.removeIf() method, as shown below. The code computes the same result as the stream pipeline in Figure 16.3. Note that the predicate in the remove() method call is a negation of the predicate in the filter() operation.
List<CD> popularCDs2 = new ArrayList<>(CD.cdList); popularCDs2.removeIf(cd -> !(popularTitles.contains(cd.title()))); System.out.println("Query 1b: " + popularCDs2); //Query 1b: [<Jaav, "Java Jive", 8, 2017, POP>, <Jaav, "Java Jam", 6, 2017, JAZZ>]
In summary, the filter() method implements a stateless intermediate operation. It can change the size of the stream, since elements are discarded. However, the element type of the output stream returned by the filter() method is the same as that of its input stream. In Figure 16.3, the input and output stream type of the filter() method is Stream<CD>. Also, the encounter order of the stream remains unchanged. In Figure 16.3, the encounter order in the output stream returned by the filter() method is the same as the order of the elements in the input stream—that is, the insertion order in the list of CDs.
Figure 16.3 Filtering Stream Elements
Taking and Dropping Elements Using Predicates
Both the takeWhile() and the dropWhile() methods find the longest prefix of elements to take or drop from the input stream, respectively.
The code below at (1) and (2) illustrates the case for ordered streams. The take-While() method takes odd numbers from the input stream until a number is not odd, and short-circuits the processing of the stream—that is, it truncates the rest of the stream based on the predicate. The dropWhile() method, on the other hand, drops odd numbers from the input stream until a number is not odd, and passes the remaining elements to its output stream; that is, it skips elements in the beginning of the stream based on the predicate.
// Ordered stream: Stream.of(1, 3, 5, 7, 8, 9, 11) // (1) .takeWhile(n -> n % 2 != 0) // Takes longest prefix: 1 3 5 7 .forEach(n -> System.out.print(n + " ")); // 1 3 5 7 Stream.of(1, 3, 5, 7, 8, 9, 11) // (2) .dropWhile(n -> n % 2 != 0) // Drops longest prefix: 1 3 5 7 .forEach(n -> System.out.print(n + " ")); // 8 9 11
Given an unordered stream, as shown below at (3), both methods return nondeterministic results: Any subset of matching elements can be taken or dropped, respectively.
// Unordered stream: Set<Integer> iSeq = Set.of(1, 9, 4, 3, 7); // (3) iSeq.stream() .takeWhile(n -> n % 2 != 0) // Takes any subset of elements. .forEach(n -> System.out.print(n + " ")); // Nondeterministic: 1 9 7 iSeq.stream() .dropWhile(n -> n % 2 != 0) // Drops any subset of elements. .forEach(n -> System.out.print(n + " ")); // Nondeterministic: 4 3
Regardless of whether the stream is ordered or unordered, if all elements match the predicate, the takeWhile() method takes all the elements and the dropWhile() method drops all the elements, as shown below at (4) and (5).
// All match in ordered stream: (4) Stream.of(1, 3, 5, 7, 9, 11) .takeWhile(n -> n % 2 != 0) // Takes all elements. .forEach(n -> System.out.print(n + " ")); // Ordered: 1 3 5 7 9 11 Stream.of(1, 3, 5, 7, 9, 11) .dropWhile(n -> n % 2 != 0) // Drops all elements. .forEach(n -> System.out.print(n + " ")); // Empty stream // All match in unordered stream: (5) Set<Integer> iSeq2 = Set.of(1, 9, 3, 7, 11, 5); iSeq2.stream() .takeWhile(n -> n % 2 != 0) // Takes all elements. .forEach(n -> System.out.print(n + " ")); // Unordered: 9 11 1 3 5 7 iSeq2.stream() .dropWhile(n -> n % 2 != 0) // Drops all elements. .forEach(n -> System.out.print(n + " ")); // Empty stream
Regardless of whether the stream is ordered or unordered, if no elements match the predicate, the takeWhile() method takes no elements and the dropWhile() method drops no elements, as shown below at (6) and (7).
// No match in ordered stream: (6) Stream.of(2, 4, 6, 8, 10, 12) .takeWhile(n -> n % 2 != 0) // Takes no elements. .forEach(n -> System.out.print(n + " ")); // Empty stream Stream.of(2, 4, 6, 8, 10, 12) .dropWhile(n -> n % 2 != 0) // Drops no elements. .forEach(n -> System.out.print(n + " ")); // Ordered: 2 4 6 8 10 12 // No match in unordered stream: (7) Set<Integer> iSeq3 = Set.of(2, 10, 8, 12, 4, 6); iSeq3.stream() .takeWhile(n -> n % 2 != 0) // Takes no elements. .forEach(n -> System.out.print(n + " ")); // Empty stream iSeq3.stream() .dropWhile(n -> n % 2 != 0) // Drops no elements. .forEach(n -> System.out.print(n + " ")); // Unordered: 8 10 12 2 4 6
Selecting Distinct Elements
The distinct() method removes all duplicates of an element from the input stream, resulting in an output stream with only unique elements. Since the distinct() method must be able to distinguish the elements from one another and keep track of them, the stream elements must override the equals() and the hashCode() methods of the Object class. The CD objects comply with this requirement (Example 16.1, p. 883).
In Figure 16.4, Query 2 creates a list of unique CDs with pop music. The filter() operation and the distinct() operation in the stream pipeline select the CDs with pop music and those that are unique, respectively. The execution of the stream pipeline shows that the resulting list of unique CDs with pop music has only one CD (cd0).
In Figure 16.4, interchanging the stateless filter() operation and the stateful distinct() operation in the stream pipeline gives the same results, but then the more expensive distinct() operation is performed on all elements of the stream, rather than on a shorter stream which is returned by the filter() operation.
Figure 16.4 Selecting Distinct Elements
Skipping Elements in a Stream
The skip() operation slices off or discards a specified number of elements from the head of a stream before the remaining elements are made available to the next operation. It preserves the encounter order if the input stream has one. Not surprisingly, skipping more elements than are in the input stream returns the empty stream.
In Figure 16.5, Query 3a creates a list of jazz music CDs after skipping the first two CDs in the stream. The stream pipeline uses a skip() operation first to discard two CDs (one of them being a jazz music CD) and a filter() operation afterward to select any CDs with jazz music. The execution of this stream pipeline shows that the resulting list contains two CDs (cd3, cd4).
In the stream pipeline in Figure 16.5, the skip() operation is before the filter() operation. Switching the order of the skip() and filter() operations as in Query 3b in Example 16.5 does not solve the same query. It will skip the first two jazz music CDs selected by the filter() operation.
Figure 16.5 Skipping Elements at the Head of a Stream
Truncating a Stream
The limit() operation returns an output stream whose maximum size is equal to the max size specified as an argument to the method. The input stream is only truncated if its size is greater than the specified max size.
In Figure 16.6, Query 4 creates a list with the first two CDs that were released in 2018. The stream pipeline uses a filter() operation first to select CDs released in 2018, and the limit() operation truncates the stream, if necessary, so that, at most, only two CDs are passed to its output stream. The short-circuit execution of this stream pipeline is illustrated in Figure 16.6, showing the resulting list containing two CDs (cd2, cd3). The execution of the stream pipeline terminates after the limit() operation has reached its limit if there are no more elements left to process. In Figure 16.6, we can see that the limit was reached and execution was terminated. Regardless of the fact that the last element in the initial stream was not processed, the stream cannot be reused once the execution of the pipeline terminates due to a short-circuiting operation.
Figure 16.6 Truncating a Stream
The limit() operation is ideal for turning an infinite stream into a finite stream. Numerous examples of using the limit() operation with the iterate() and generate() methods can be found in §16.4, p. 894, and with the Random.ints() method in §16.4, p. 900.
For a given value n, limit(n) and skip(n) are complementary operations on a stream, as limit(n) comprises the first n elements of the stream and skip(n) comprises the remaining elements in the stream. In the code below, the resultList from processing the resulting stream from concatenating the two substreams is equal to the stream source CD.cdList.
List<CD> resultList = Stream .concat(CD.cdList.stream().limit(2), CD.cdList.stream().skip(2)) .toList(); System.out.println(CD.cdList.equals(resultList)); // true
The skip() operation can be used in conjunction with the limit() operation to process a substream of a stream, where the skip() operation can be used to skip to the start of the substream and the limit() operation to limit the size of the substream. The substream in the code below starts at the second element and comprises the next three elements in the stream.
List<CD> substream = CD.cdList .stream() .skip(1) .limit(3) .toList(); System.out.println("Query 5: " + substream); // Query 5: [<Jaav, "Java Jam", 6, 2017, JAZZ>, // <Funkies, "Lambda Dancing", 10, 2018, POP>, // <Genericos, "Keep on Erasing", 8, 2018, JAZZ>]
The limit() operation is a short-circuiting stateful intermediate operation, as it needs to keep state for tracking the number of elements in the output stream. It changes the stream size, but not the stream element type or the encounter order. For an ordered stream, we can expect the elements in the resulting stream to have the same order, but we cannot assume any order if the input stream is unordered.
Example 16.5 contains the code snippets presented in this subsection.
Example 16.5 Filtering
import java.time.Year; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.stream.Stream; public final class Filtering { public static void main(String[] args) { // Query 1: Find CDs whose titles are in the set of popular CD titles. Set<String> popularTitles = Set.of("Java Jive", "Java Jazz", "Java Jam"); // Using Stream.filter(). List<CD> popularCDs1 = CD.cdList .stream() .filter(cd -> popularTitles.contains(cd.title())) .toList(); System.out.println("Query 1a: " + popularCDs1); // Using Collection.removeIf(). List<CD> popularCDs2 = new ArrayList<>(CD.cdList); popularCDs2.removeIf(cd -> !(popularTitles.contains(cd.title()))); System.out.println("Query 1b: " + popularCDs2); // Query 2: Create a list of unique CDs with pop music. List<CD> miscCDList = List.of(CD.cd0, CD.cd0, CD.cd1, CD.cd0); List<CD> uniquePopCDs1 = miscCDList .stream() .filter(CD::isPop) .distinct() // distinct() after filter() .toList(); System.out.println("Query 2: " + uniquePopCDs1); // Query 3a: Create a list of jazz CDs, after skipping the first two CDs. List<CD> jazzCDs1 = CD.cdList .stream() .skip(2) // skip() before filter(). .filter(CD::isJazz) .toList(); System.out.println("Query 3a: " + jazzCDs1); // Query 3b: Create a list of jazz CDs, but skip the first two jazz CDs. List<CD> jazzCDs2 = CD.cdList // Not equivalent to Query 3 .stream() .filter(CD::isJazz) .skip(2) // skip() after filter(). .toList(); System.out.println("Query 3b: " + jazzCDs2); // Query 4: Create a list with the first 2 CDs that were released in 2018. List<CD> twoFirstCDs2018 = CD.cdList .stream() .filter(cd -> cd.year().equals(Year.of(2018))) .limit(2) .toList(); System.out.println("Query 4: " + twoFirstCDs2018); // limit(n) and skip(n) are complementary. List<CD> resultList = Stream .concat(CD.cdList.stream().limit(2), CD.cdList.stream().skip(2)) .toList(); System.out.println(CD.cdList.equals(resultList)); // Query 5: Process a substream by skipping 1 and limiting the size to 3. List<CD> substream = CD.cdList .stream() .skip(1) .limit(3) .toList(); System.out.println("Query 5: " + substream); } }
Output from the program (formatted to fit on the page):
Query 1a: [<Jaav, "Java Jive", 8, 2017, POP>, <Jaav, "Java Jam", 6, 2017, JAZZ>] Query 1b: [<Jaav, "Java Jive", 8, 2017, POP>, <Jaav, "Java Jam", 6, 2017, JAZZ>] Query 2: [<Jaav, "Java Jive", 8, 2017, POP>] Query 3a: [<Genericos, "Keep on Erasing", 8, 2018, JAZZ>, <Genericos, "Hot Generics", 10, 2018, JAZZ>] Query 3b: [<Genericos, "Hot Generics", 10, 2018, JAZZ>] Query 4: [<Funkies, "Lambda Dancing", 10, 2018, POP>, <Genericos, "Keep on Erasing", 8, 2018, JAZZ>] true Query 5: [<Jaav, "Java Jam", 6, 2017, JAZZ>, <Funkies, "Lambda Dancing", 10, 2018, POP>, <Genericos, "Keep on Erasing", 8, 2018, JAZZ>]
Examining Elements in a Stream
The peek() operation allows stream elements to be examined at the point where the operation is used in the stream pipeline. It does not affect the stream in any way, as it only facilitates a side effect via a non-interfering consumer specified as an argument to the operation. It is primarily used for debugging the pipeline by examining the elements at various points in the pipeline.
The following method is defined in the Stream<T> interface, and an analogous method is also defined in the IntStream, LongStream, and DoubleStream interfaces:
By using the peek() method, we can dispense with explicit print statements that were inserted in the implementation of the behavioral parameter of the map() operation in Example 16.4, p. 909. Example 16.6 shows how the peek() operation can be used to trace the processing of elements in the pipeline. A peek() operation after each intermediate operation prints pertinent information which can be used to verify the workings of the pipeline. In Example 16.6, the output shows that the skip() operation before the map() operation can improve performance, as the skip() operation shortens the stream on which the map() operation should be performed.
Example 16.6 Examining Stream Elements
import java.util.List; public final class OrderOfOperationsWithPeek { public static void main(String[] args) { System.out.println("map() before skip():"); List<String> cdTitles1 = CD.cdList .stream() .map(CD::title) .peek(t -> System.out.println("After map: " + t)) .skip(3) .peek(t -> System.out.println("After skip: " + t)) .toList(); System.out.println(cdTitles1); System.out.println(); System.out.println("skip() before map():"); // Preferable. List<String> cdTitles2 = CD.cdList .stream() .skip(3) .peek(cd -> System.out.println("After skip: " + cd)) .map(CD::title) .peek(t -> System.out.println("After map: " + t)) .toList(); System.out.println(cdTitles2); } }
Output from the program:
map() before skip(): After map: Java Jive After map: Java Jam After map: Lambda Dancing After map: Keep on Erasing After skip: Keep on Erasing After map: Hot Generics After skip: Hot Generics [Keep on Erasing, Hot Generics] skip() before map(): After skip: <Genericos, "Keep on Erasing", 8, 2018, JAZZ> After map: Keep on Erasing After skip: <Genericos, "Hot Generics", 10, 2018, JAZZ> After map: Hot Generics [Keep on Erasing, Hot Generics]
Mapping: Transforming Streams
The map() operation has already been used in several examples (Example 16.3, p. 906, Example 16.4, p. 909, and Example 16.6, p. 920). Here we take a closer look at this essential intermediate operation for data processing using a stream. It maps one type of stream (Stream<T>) into another type of stream (Stream<R>); that is, each element of type T in the input stream is mapped to an element of type R in the output stream by the function (Function<T, R>) supplied to the map() method. It defines a one-to-one mapping. For example, if we are interested in the titles of CDs in the CD stream, we can use the map() operation to transform each CD in the stream to a String that represents the title of the CD by applying an appropriate function:
Stream<String> titles = CD.cdList .stream() // Input stream: Stream<CD>. .map(CD::title); // Lambda expression: cd -> cd.title()
The following methods are defined in the Stream<T> interface, and analogous methods are also defined in the IntStream, LongStream, and DoubleStream interfaces:
In Figure 16.7, the query creates a list with CD titles released in 2018. The stream pipeline uses a filter() operation first to select CDs released in 2018, and the map() operation maps a CD to its title (String). The input stream is transformed by the map() operation from Stream<CD> to Stream<String>. The execution of this stream pipeline shows the resulting list (List<String>) containing three CD titles.
Figure 16.7 Mapping
The query below illustrates transforming an object stream to a numeric stream. When executed, the stream pipeline prints the years in which the CDs were released. Note the transformation of the initial stream, Stream<CD>. The map() operation first transforms it to a Stream<Year> and the distinct() operation selects the unique years. The mapToInt() operation transforms the stream from Stream<Year> to IntStream—that is, a stream of ints whose values are then printed.
CD.cdList.stream() // Stream<CD> .map(CD::year) // Stream<Year> .distinct() // Stream<Year> .mapToInt(Year::getValue) // IntStream .forEach(System.out::println); // 2017 // 2018
In the example below, the range() method generates an int stream for values in the half-open interval specified by its arguments. The values are generated in increasing order, starting with the lower bound of the interval. In order to generate them in decreasing order, the map() operation can be used to reverse the values. In this case, the input stream and output stream of the map() operation are both IntStreams.
int from = 0, to = 5; IntStream.range(from, to) // [0, 5) .map(i -> to + from - 1 - i) // Reverse the stream values .forEach(System.out::print); // 43210
The stream pipeline below determines the number of times the dice value is 6. The generate() method generates a value between 1 and 6, and the limit() operation limits the max size of the stream. The map() operation returns the value 1 if the dice value is 6; otherwise, it returns 0. In other words, the value of the dice throw is mapped either to 1 or 0, depending on the dice value. The terminal operation sum() sums the values in the streams, which in this case are either 1 or 0, thus returning the correct number of times the dice value was 6.
long sixes = IntStream .generate(() -> (int) (6.0 * Math.random()) + 1) // [1, 6] .limit(2000) // Number of throws. .map(i -> i == 6 ? 1 : 0) // Dice value mapped to 1 or 0. .sum();
Flattening Streams
The flatMap() operation first maps each element in the input stream to a mapped stream, and then flattens the mapped streams to a single stream—that is, the elements of each mapped stream are incorporated into a single stream when the pipeline is executed. In other words, each element in the input stream may be mapped to many elements in the output stream. The flatMap() operation thus defines a one-to-many mapping that flattens a multilevel stream by one level.
The following method is defined in the Stream<T> interface, and an analogous method is also defined in the IntStream, LongStream, and DoubleStream interfaces:
The methods below are defined only in the Stream<T> interface. No counterparts exist in the IntStream, LongStream, or DoubleStream interfaces:
To motivate using the flatMap() operation, we look at how to express the query for creating a list of unique CDs from two given lists of CDs. Figure 16.8 shows an attempt to express this query by creating a stream of lists of CDs, Stream<List<CD>>, and selecting the unique CDs using the distinct() method. This attempt fails miserably as the distinct() method distinguishes between elements that are lists of CDs, and not individual CDs. Figure 16.8 shows the execution of the stream pipeline resulting in a list of lists of CDs, List<List<CD>>.
Figure 16.8 Incorrect Solution to the Query
The next attempt to express the query uses the map() operation as shown in Figure 16.9. The idea is to map each list of CDs (List<CD>) to a stream of CDs (Stream<CD>), and select the unique CDs with the distinct() operation. The mapper function of the map() operation maps each list of CDs to a mapped stream that is a stream of CDs, Stream<CD>. The resulting stream from the map() operation is a stream of streams of CDs, Stream<Stream<CD>>. The distinct() method distinguishes between elements that are mapped streams of CDs. Figure 16.9 shows the execution of the stream pipeline resulting in a list of mapped streams of CDs, List<Stream<CD>>.
Figure 16.9 Mapping a Stream of Streams
The flatMap() operation provides the solution, as it flattens the contents of the mapped streams into a single stream so that the distinct() operation can select the unique CDs individually. The stream pipeline using the flatMap() operation and its execution are shown in Figure 16.10. The mapper function of the flatMap() operation maps each list of CDs to a mapped stream that is a stream of CDs, Stream<CD>. The contents of the mapped stream are flattened into the output stream. The resulting stream from the flatMap() operation is a stream of CDs, Stream<CD>. Note how each list in the initial stream results in a flattened stream whose elements are processed by the pipeline. The result list of CDs contains the unique CDs from the two lists.
Figure 16.10 Flattening Streams
The code below flattens a two-dimensional array to a one-dimensional array. The Arrays.stream() method call at (1) creates an object stream, Stream<int[]>, whose elements are arrays that are rows in the two-dimensional array. The mapper of the flatMapToInt() operation maps each row in the Stream<int[]> to a stream of ints (IntStream) by applying the Array.stream() method at (2) to each row. This would result in a stream of mapped streams of ints (Stream<IntStream>>), but it is flattened by the flatMapToInt() operation to a final stream of ints (IntStream). The terminal operation toArray() creates an appropriate array in which the int values of the final stream are stored (p. 971).
int[][] twoDimArray = { {2017, 2018}, {1948, 1949} }; int[] intArray = Arrays .stream(twoDimArray) // (1) Stream<int[]> .flatMapToInt(row -> Arrays.stream(row)) // (2) mapper: int[] -> IntStream, // flattens Stream<IntStream> to IntStream. .toArray(); // [2017, 2018, 1948, 1949]
Replacing Each Element of a Stream with Multiple Elements
The mapMulti() intermediate operation applies a one-to-many transformation to the elements of the stream and flattens the result elements into a new stream. The functionality of the mapMulti() method is very similar to that of the flatMap() method. Whereas the latter uses a Function<T, Stream<R>> mapper to create a mapping stream for each element and then flattens the stream, the former applies a BiConsumer<T, Consumer<R>> mapper to each element. The mapper calls the Consumer to accept the replacement elements that are incorporated into a single stream when the pipeline is executed.
The mapMulti() method can be used to perform filtering, mapping, and flat mapping of stream elements, all depending on the implementation of the BiConsumer mapper passed to the method.
The code below shows a one-to-one transformation of the stream elements. A BiConsumer is defined at (1) that first filters the stream for pop music CDs at (2), and maps each CD to a string that contains its title and its number of tracks represented by an equivalent number of "*" characters. The resulting string is submitted at (3) to the consumer (supplied by the mapMulti() method). Each value passed to the accept() method of the consumer replaces the current element in the stream. Note that the body of the BiConsumer is implemented in an imperative manner using an if statement. The BiConsumer created at (1) is passed to the mapMulti() method at (5) to process the CDs of the stream created at (4). The mapMulti() method passes an appropriate Consumer to the BiConsumer that accepts the replacement elements.
// One-to-one BiConsumer<CD, Consumer<String>> bcA = (cd, consumer) -> { // (1) if (cd.genre() == Genre.POP) { // (2) consumer.accept(String.format("%-15s: %s", cd.title(), // (3) "*".repeat(cd.noOfTracks()))); } }; CD.cdList.stream() // (4) .mapMulti(bcA) // (5) .forEach(System.out::println);
Output from the code:
Java Jive : ******** Lambda Dancing : **********
The code below shows a one-to-many transformation of the stream elements. The BiConsumer at (1) iterates through a list of CDs and maps each CD in the list to its title. Each list of CDs in the stream will thus be replaced with the titles of the CDs in the list. The mapMulti() operation with the BiConsumer at (1) is applied at (3) to a stream of list of CDs (Stream<List<CD>>) created at (2). The mapMulti() operation in this case is analogous to the flatMap() operation to achieve the same result.
// One-to-many List<CD> cdList1 = List.of(CD.cd0, CD.cd1, CD.cd1); List<CD> cdList2 = List.of(CD.cd0, CD.cd1); BiConsumer<List<CD>, Consumer<String>> bcB = (lst, consumer) -> { // (1) for (CD cd : lst) { consumer.accept(cd.title()); } }; List<String> listOfCDTitles = Stream.of(cdList1, cdList2) // (2) Stream<List<CD>> .mapMulti(bcB) // (3) .distinct() .toList(); System.out.println(listOfCDTitles); // [Java Jive, Java Jam]
The previous two code snippets first defined the BiConsumer with all relevant types specified explicitly, and then passed it to the mapMulti() method. The code below defines the implementation of the BiConsumer in the call to the mapMulti() method. We consider three alternative implementations as exemplified by (2a), (2b), and (2c).
Alternative (2a) results in a compile-time error. The reason is that the compiler cannot unequivocally infer the actual type parameter R of the consumer parameter of the lambda expression. It can only infer that the type of the lst parameter is List<CD> as it denotes an element of stream whose type is Stream<List<CD>>. The compiler makes the safest assumption that the type parameter R is Object. With this assumption, the resulting list is of type List<Object>, but this cannot be assigned to a reference of type List<String>, as declared in the assignment statement. To avoid the compile-time error in this case, we can change the type of the reference to Object or to the wildcard ?.
Alternative (2b) uses the type witness <String> in the call to the mapMulti() method to explicitly corroborate the actual type parameter of the consumer.
Alternative (2c) explicitly specifies the types for the parameters of the lambda expression.
List<String> listOfCDTitles2 = Stream.of(cdList1,cdList2) // (1) Stream<List<CD>> // .mapMulti((lst, consumer) -> { // (2a) Compile-time error! // .<String>mapMulti((lst, consumer) -> { // (2b) OK. .mapMulti((List<CD> lst, Consumer<String> consumer) -> { // (2c) OK. for (CD cd : lst) { consumer.accept(cd.title()); } }) .distinct() .toList(); System.out.println(listOfCDTitles2); // [Java Jive, Java Jam]
The mapMulti() method is preferable to the flatMap() method under the following circumstances:
When an element is to be replaced with a small number of elements, or none at all. The mapMulti() method avoids the overhead of creating a mapped stream for each element, as done by the flatMap() method.
When an imperative approach for creating replacement elements is easier than using a stream.
The following default method is defined in the Stream<T> interface, and an analogous method is also defined in the IntStream, LongStream, and DoubleStream interfaces:
The following default methods are defined only in the Stream<T> interface. No counterparts exist in the IntStream, LongStream, or DoubleStream interfaces:
Sorted Streams
The sorted() intermediate operation can be used to enforce a specific encounter order on the elements of the stream. It is important to note that the data source is not sorted; only the order of the elements in the stream is affected when a stream is sorted. It is an expensive stateful operation, as state must be kept for all elements in the stream before making them available in the resulting stream.
The following methods are defined in the Stream<T> interface, but only the first method is defined in the IntStream, LongStream, and DoubleStream interfaces:
The Comparable<E> and Comparator<E> interfaces are covered in §14.4, p. 761, and §14.5, p. 769, respectively.
Example 16.7 illustrates the sorted() operation on streams. Printing the array at (1) and executing the stream pipeline at (2) shows that the order of the elements in the array and in the stream is positional order, as one would expect. The zero-argument sorted() method sorts in natural order, as in the pipeline at (3). It expects the stream elements to implement the Comparable<CD> interface. The sorted() method in the pipeline at (4) uses the reverse natural order to sort the elements.
The pipeline at (5) represents the query to find all jazz music CDs and sort them by their title. A comparator to compare by title is passed to the sorted() method. Finally, the pipeline at (6) finds CDs with eight or more tracks, and sorts them according to the number of tracks. An appropriate comparator that compares by the number of tracks is passed to the sorted() method.
It is instructive to compare the output showing the results from each pipeline in Example 16.7. The comparators in Example 16.7 are also implemented as lambda expressions, in addition to their implementation by the methods in the Comparator<E> interface.
Example 16.7 Sorting Streams
import java.util.Arrays; import java.util.Comparator; import java.util.List; public class Sorting { public static void main(String[] args) { System.out.println("(1) Positional order in the array:"); CD[] cdArray = CD.cdArray; System.out.println(Arrays.toString(cdArray)); // (1) System.out.println("(2) Positional order in the stream:"); List<CD> cdsByPositionalOrder = // (2) Arrays.stream(cdArray) .toList(); System.out.println(cdsByPositionalOrder); System.out.println("(3) Natural order:"); List<CD> cdsByNaturalOrder = // (3) Arrays.stream(cdArray) .sorted() .toList(); System.out.println(cdsByNaturalOrder); System.out.println("(4) Reversed natural order:"); List<CD> cdsByRNO = // (4) Arrays.stream(cdArray) // .sorted((c1, c2) -> -c1.compareTo(c2)) .sorted(Comparator.reverseOrder()) .toList(); System.out.println(cdsByRNO); System.out.println("(5) Only Jazz CDs, ordered by title:"); List<String> jazzCDsByTitle = // (5) Arrays.stream(cdArray) .filter(CD::isJazz) // .sorted((c1, c2) -> c1.title().compareTo(c2.title())) .sorted(Comparator.comparing(CD::title)) .map(CD::title) .toList(); System.out.println(jazzCDsByTitle); System.out.println("(6) No. of tracks >= 8, ordered by number of tracks:"); List<CD> cds = // (6) Arrays.stream(cdArray) .filter(d -> d.noOfTracks() >= 8) // .sorted((c1, c2) -> c1.noOfTracks() - c2.noOfTracks()) .sorted(Comparator.comparing(CD::noOfTracks)) .toList(); System.out.println(cds); } }
Output from the program (formatted to fit on the page):
(1) Positional order in the array: [<Jaav, "Java Jive", 8, 2017, POP>, <Jaav, "Java Jam", 6, 2017, JAZZ>, <Funkies, "Lambda Dancing", 10, 2018, POP>, <Genericos, "Keep on Erasing", 8, 2018, JAZZ>, <Genericos, "Hot Generics", 10, 2018, JAZZ>] (2) Positional order in the stream: [<Jaav, "Java Jive", 8, 2017, POP>, <Jaav, "Java Jam", 6, 2017, JAZZ>, <Funkies, "Lambda Dancing", 10, 2018, POP>, <Genericos, "Keep on Erasing", 8, 2018, JAZZ>, <Genericos, "Hot Generics", 10, 2018, JAZZ>] (3) Natural order: [<Funkies, "Lambda Dancing", 10, 2018, POP>, <Genericos, "Hot Generics", 10, 2018, JAZZ>, <Genericos, "Keep on Erasing", 8, 2018, JAZZ>, <Jaav, "Java Jam", 6, 2017, JAZZ>, <Jaav, "Java Jive", 8, 2017, POP>] (4) Reversed natural order: [<Jaav, "Java Jive", 8, 2017, POP>, <Jaav, "Java Jam", 6, 2017, JAZZ>, <Genericos, "Keep on Erasing", 8, 2018, JAZZ>, <Genericos, "Hot Generics", 10, 2018, JAZZ>, <Funkies, "Lambda Dancing", 10, 2018, POP>] (5) Only Jazz CDs, ordered by title: [Hot Generics, Java Jam, Keep on Erasing] (6) No. of tracks >= 8, ordered by number of tracks: [<Jaav, "Java Jive", 8, 2017, POP>, <Genericos, "Keep on Erasing", 8, 2018, JAZZ>, <Funkies, "Lambda Dancing", 10, 2018, POP>, <Genericos, "Hot Generics", 10, 2018, JAZZ>]
Setting a Stream as Unordered
The unordered() intermediate operation does not actually reorder the elements in the stream to make them unordered. It just removes the ordered constraint on a stream if this constraint is set for the stream, indicating that stream operations can choose to ignore its encounter order. Indicating the stream to be unordered can improve the performance of some operations. For example, the limit(), skip(), and distinct() operations can improve performance when executed on unordered parallel streams, since they can process any elements by ignoring the encounter order. The removal of the ordered constraint can impact the performance of certain operations on parallel streams (p. 1015).
It clearly makes sense to call the unordered() method on an ordered stream only if the order is of no consequence in the final result. There is no method called ordered to impose an order on a stream. However, the sorted() intermediate operation can be used to enforce a sort order on the output stream.
In the stream pipeline below, the unordered() method clears the ordered constraint on the stream whose elements have the same order as in the data source—that is, the positional order in the list of CDs. The outcome of the execution shows that the titles in the result list are in the same order as they are in the data source; this is the same result one would get without the unordered() operation. It is up to the stream operation to take into consideration that the stream is unordered. The fact that the result list retains the order does not make it invalid. After all, since the stream is set as unordered, it indicates that ignoring the order is at the discretion of the stream operation.
//Query: Create a list with the first 2 Jazz CD titles. List<String> first2JazzCDTitles = CD.cdList .stream() .unordered() // Don't care about ordering. .filter(CD::isJazz) .limit(2) .map(CD::title) .toList(); // [Java Jam, Keep on Erasing]
The following method is inherited by the Stream<T> interface from its superinterface BaseStream. Analogous methods are also inherited by the IntStream, LongStream, and DoubleStream interfaces from the superinterface BaseStream.
Execution Mode of a Stream
The two methods parallel() and sequential() are intermediate operations that can be used to set the execution mode of a stream—that is, whether it will execute sequentially or in parallel. Only the Collection.parallelStream() method creates a parallel stream from a collection, so the default mode of execution for most streams is sequential, unless the mode is specifically changed by calling the parallel() method. The execution mode of a stream can be switched between sequential and parallel execution at any point between stream creation and the terminal operation in the pipeline. However, it is the last call to any of these methods that determines the execution mode for the entire pipeline, regardless of how many times these methods are called in the pipeline.
The declaration statements below show examples of both sequential and parallel streams. No stream pipeline is executed, as no terminal operation is invoked on any of the streams. However, when a terminal operation is invoked on one of the streams, the stream will be executed in the mode indicated for the stream.
Stream<CD> seqStream1 = CD.cdList.stream().filter(CD::isPop); // Sequential Stream<CD> seqStream2 = CD.cdList.stream().sequential().filter(CD::isPop); // Sequential Stream<CD> seqStream3 = CD.cdList.stream().parallel().filter(CD::isPop).sequential(); // Sequential Stream<CD> paraStream1 = CD.cdList.stream().parallel().filter(CD::isPop); // Parallel Stream<CD> paraStream2 = CD.cdList.stream().filter(CD::isPop).parallel(); // Parallel
The isParallel() method can be used to determine the execution mode of a stream. For example, the call to the isParallel() method on seqStream3 below shows that this stream is a sequential stream. It is the call to the sequential() method that occurs last in the pipeline that determines the execution mode.
System.out.println(seqStream3.isParallel()); // false
Parallel streams are explored further in §16.9, p. 1009.
The following methods are inherited by the Stream<T> interface from its superinterface BaseStream. Analogous methods are also inherited by the IntStream, LongStream, and DoubleStream interfaces from the superinterface BaseStream.
Converting between Stream Types
Table 16.2 provides a summary of interoperability between stream types—that is, transforming between different stream types. Where necessary, the methods are shown with the name of the built-in functional interface required as a parameter. Selecting a naming convention for method names makes it easy to select the right method for transforming one stream type to another.
Table 16.2 Interoperability between Stream Types
Stream types |
To Stream<R> |
To IntStream |
To LongStream |
To DoubleStream |
---|---|---|---|---|
From Stream<T> |
map(Function) |
mapToInt(ToIntFunction) |
mapToLong( ToLongFunction) |
mapToDouble(ToDoubleStream) |
flatMap(Function) |
flatMapToInt(Function) |
flatMapToLong(Function) |
flatMapToDouble(Function) |
|
From IntStream |
mapToObj(IntFunction) |
map(IntUnary-Operator) |
mapToLong(IntToLong-Function) |
mapToDouble(IntToDouble-Function) |
Stream<Integer> boxed() |
flatMap(IntFunction) |
asLongStream() |
asDoubleStream() |
|
From LongStream |
mapToObj(LongFunction) |
mapToInt(LongToInt-Function) |
map(DoubleUnary-Operator) |
mapToDouble(LongToDouble-Function) |
Stream<Long> boxed() |
|
flatMap(DoubleFunction) |
asDoubleStream() |
|
From DoubleStream |
mapToObj(DoubleFunction) |
mapToInt(DoubleToInt-Function) |
mapToLong(DoubleToLong-Function) |
map(DoubleUnary-Operator) |
Stream<Double> boxed() |
|
|
flatMap(DoubleFunction) |
Mapping between Object Streams
The map() and flatMap() methods of the Stream<T> interface transform an object stream of type T to an object stream of type R. Examples using these two methods can be found in §16.5, p. 921, and §16.5, p. 924, respectively.
Mapping an Object Stream to a Numeric Stream
The mapToNumType() methods in the Stream<T> interface transform an object stream to a stream of the designated numeric type, where NumType is either Int, Long, or Double.
The query below sums the number of tracks for all CDs in a list. The mapToInt() intermediate operation at (2) accepts an IntFunction that extracts the number of tracks in a CD, thereby transforming the Stream<CD> created at (1) into an IntStream. The terminal operation sum(), as the name implies, sums the values in the IntStream (p. 973).
int totalNumOfTracks = CD.cdList .stream() // (1) Stream<CD> .mapToInt(CD::noOfTracks) // (2) IntStream .sum(); // 42
The flatMapToNumType() methods are only defined by the Stream<T> interface to flatten a multilevel object stream to a numeric stream, where NumType is either Int, Long, or Double.
Earlier we saw an example of flattening a two-dimensional array using the flat-MapToInt() method (p. 924).
The query below sums the number of tracks for all CDs in two CD lists. The flatMapToInt() intermediate operation at (1) accepts a Function that maps each List<CD> in a Stream<List<CD>> to an IntStream whose values are the number of tracks in a CD contained in the list. The resulting Stream<IntStream> from the mapper function is flattened into an IntStream by the flatMapToInt() intermediate operation, thus transforming the initial Stream<List<CD>> into an IntStream. The terminal operation sum() sums the values in this IntStream (p. 973).
List<CD> cdList1 = List.of(CD.cd0, CD.cd1); List<CD> cdList2 = List.of(CD.cd2, CD.cd3, CD.cd4); int totalNumOfTracks = Stream.of(cdList1, cdList2) // Stream<List<CD>> .flatMapToInt( // (1) lst -> lst.stream() // Stream<CD> .mapToInt(CD::noOfTracks)) // IntStream // Stream<IntStream>, // flattened to IntStream. .sum(); // 42
Mapping a Numeric Stream to an Object Stream
The mapToObj() method defined by the numeric stream interfaces transforms a numeric stream to an object stream of type R, and the boxed() method transforms a numeric stream to an object stream of its wrapper class.
The query below prints the squares of numbers in a given closed range, where the number and its square are stored as a pair in a list of size 2. The mapToObj() intermediate operation at (2) transforms an IntStream created at (1) to a Stream<List<Integer>>. Each list in the result stream is then printed by the forEach() terminal operation.
IntStream.rangeClosed(1, 3) // (1) IntStream .mapToObj(n -> List.of(n, n*n)) // (2) Stream<List<Integer>> .forEach(p -> System.out.print(p + " ")); // [1, 1] [2, 4] [3, 9]
The query above can also be expressed as shown below. The boxed() intermediate operation transforms the IntStream at (3) into a Stream<Integer> at (4); in other words, each int value is boxed into an Integer which is then mapped by the map() operation at (5) to a List<Integer>, resulting in a Stream<List<Integer>> as before. The compiler will issue an error if the boxed() operation is omitted at (4), as the map() operation at (5) will be invoked on an IntStream, expecting an IntUnaryFunction, which is not the case.
IntStream.rangeClosed(1, 3) // (3) IntStream .boxed() // (4) Stream<Integer> .map(n -> List.of(n, n*n)) // (5) Stream<List<Integer>> .forEach(p -> System.out.print(p + " ")); // [1, 1] [2, 4] [3, 9]
The examples above show that the IntStream.mapToObj() method is equivalent to the IntStream.boxed() method followed by the Stream.map() method.
The mapToObj() method, in conjunction with a range of int values, can be used to create sublists and subarrays. The query below creates a sublist of CD titles based on a closed range whose values are used as an index in the CD list.
List<String> subListTitles = IntStream .rangeClosed(2, 3) // IntStream .mapToObj(i -> CD.cdList.get(i).title()) // Stream<String> .toList(); // [Lambda Dancing, Keep on Erasing]
Mapping between Numeric Streams
In contrast to the methods in the Stream<T> interface, the map() and the flatMap() methods of the numeric stream interfaces transform a numeric stream to a numeric stream of the same primitive type; that is, they do not change the type of the numeric stream.
The map() operation in the stream pipeline below does not change the type of the initial IntStream.
IntStream.rangeClosed(1, 3) // IntStream .map(i -> i * i) // IntStream .forEach(n -> System.out.printf("%d ", n)); // 1 4 9
The flatMap() operation in the stream pipeline below also does not change the type of the initial stream. Each IntStream created by the mapper function is flattened, resulting in a single IntStream.
IntStream.rangeClosed(1, 3) // IntStream .flatMap(i -> IntStream.rangeClosed(1, 4)) // IntStream .forEach(n -> System.out.printf("%d ", n)); // 1 2 3 4 1 2 3 4 1 2 3 4
Analogous to the methods in the Stream<T> interface, the mapToNumType() methods in the numeric stream interfaces transform a numeric stream to a stream of the designated numeric type, where NumType is either Int, Long, or Double.
The mapToDouble() operation in the stream pipeline below transforms the initial IntStream into a DoubleStream.
IntStream.rangeClosed(1, 3) // IntStream .mapToDouble(i -> Math.sqrt(i)) // DoubleStream .forEach(d -> System.out.printf("%.2f ", d));// 1.00 1.41 1.73
The methods asLongStream() and asDoubleStream() in the IntStream interface transform an IntStream to a LongStream and a DoubleStream, respectively. Similarly, the method asDoubleStream() in the LongStream interface transforms a LongStream to a DoubleStream.
The asDoubleStream() operation in the stream pipeline below transforms the initial IntStream into a DoubleStream. Note how the range of int values is thereby transformed to a range of double values by the asDoubleStream() operation.
IntStream.rangeClosed(1, 3) // IntStream .asDoubleStream() // DoubleStream .map(d -> Math.sqrt(d)) // DoubleStream .forEach(d -> System.out.printf("%.2f ", d));// 1.00 1.41 1.73
In the stream pipeline below, the int values in the IntStream are first boxed into Integers. In other words, the initial IntStream is transformed into an object stream, Stream<Integer>. The map() operation transforms the Stream<Integer> into a Stream<Double>. In contrast to using the asDoubleStream() in the stream pipeline above, note the boxing/unboxing that occurs in the stream pipeline below in the evaluation of the Math.sqrt() method, as this method accepts a double as a parameter and returns a double value.
IntStream.rangeClosed(1, 3) // IntStream .boxed() // Stream<Integer> .map(n -> Math.sqrt(n)) // Stream<Double> .forEach(d -> System.out.printf("%.2f ", d));// 1.00 1.41 1.73
Summary of Intermediate Stream Operations
Table 16.3 summarizes selected aspects of the intermediate operations.
Table 16.3 Selected Aspects of Intermediate Stream Operations
Intermediate operation |
Stateful/Stateless |
Can change stream size |
Can change stream type |
Encounter order |
---|---|---|---|---|
distinct (p. 915) |
Stateful |
Yes |
No |
Unchanged |
dropWhile (p. 913) |
Stateful |
Yes |
No |
Unchanged |
filter (p. 910) |
Stateless |
Yes |
No |
Unchanged |
flatMap (p. 921) |
Stateless |
Yes |
Yes |
Not guaranteed |
limit (p. 917) |
Stateful, short-circuited |
Yes |
No |
Unchanged |
map (p. 921) |
Stateless |
No |
Yes |
Not guaranteed |
mapMulti (p. 927) |
Stateless |
Yes |
Yes |
Not guaranteed |
parallel (p. 933) |
Stateless |
No |
No |
Unchanged |
peek (p. 920) |
Stateless |
No |
No |
Unchanged |
sequential (p. 933) |
Stateless |
No |
No |
Unchanged |
skip (p. 915) |
Stateful |
Yes |
No |
Unchanged |
sorted (p. 929) |
Stateful |
No |
No |
Ordered |
takeWhile (p. 913) |
Stateful, short-circuited |
Yes |
No |
Unchanged |
unordered (p. 932) |
Stateless |
No |
No |
Not guaranteed |
The intermediate operations of the Stream<T> interface (including those inherited from its superinterface BaseStream<T,Stream<T>>) are summarized in Table 16.4. The type parameter declarations have been simplified, where any bounds <? super T> or <? extends T> have been replaced by <T>, without impacting the intent of a method. A reference is provided to each method in the first column. Any type parameter and return type declared by these methods are shown in column two.
The last column in Table 16.4 indicates the function type of the corresponding parameter in the previous column. It is instructive to note how the functional interface parameters provide the parameterized behavior of an operation. For example, the filter() method returns a stream whose elements satisfy a given predicate. This predicate is defined by the functional interface Predicate<T> that is implemented by a lambda expression or a method reference, and applied to each element in the stream.
The interfaces IntStream, LongStream, and DoubleStream also define analogous methods to those shown in Table 16.4, except for the flatMapToNumType() methods, where NumType is either Int, Long, or Double. A summary of additional methods defined by these numeric stream interfaces can be found in Table 16.2.
Table 16.4 Intermediate Stream Operations
Method name |
Any type parameter + return type |
Functional interface parameters |
Function type of parameters |
---|---|---|---|
distinct (p. 915) |
Stream<T> |
() |
|
dropWhile (p. 913) |
Stream<T> |
(Predicate<T> predicate) |
T -> boolean |
filter (p. 910) |
Stream<T> |
(Predicate<T> predicate) |
T -> boolean |
flatMap (p. 921) |
<R> Stream<R> |
(Function<T,Stream<R>> mapper) |
T -> Stream<R> |
flatMapToDouble (p. 921) |
DoubleStream |
(Function<T,DoubleStream> mapper) |
T -> DoubleStream |
flatMapToInt (p. 921) |
IntStream |
(Function<T,IntStream> mapper) |
T -> IntStream |
flatMapToLong (p. 921) |
LongStream |
(Function<T,LongStream> mapper) |
T -> LongStream |
limit (p. 917) |
Stream<T> |
(long maxSize) |
|
map (p. 921) |
<R> Stream<R> |
(Function<T,R> mapper) |
T -> R |
mapMulti (p. 927) |
<R> Stream<R> |
(BiConsumer<T,Consumer<R>> mapper) |
(T, Consumer<R>) -> void |
mapToDouble (p. 921) |
DoubleStream |
(ToDoubleFunction<T> mapper) |
T -> double |
mapToInt (p. 921) |
IntStream |
(ToIntFunction<T> mapper) |
T -> int |
mapToLong (p. 921) |
LongStream |
(ToLongFunction<T> mapper) |
T -> long |
parallel (p. 933) |
Stream<T> |
() |
|
peek (p. 920) |
Stream<T> |
(Consumer<T> action) |
T -> void |
sequential (p. 933) |
Stream<T> |
() |
|
skip (p. 915) |
Stream<T> |
(long n) |
|
sorted (p. 929) |
Stream<T> |
() |
|
sorted (p. 929) |
Stream<T> |
(Comparator<T> cmp) |
(T,T) -> int |
takeWhile (p. 913) |
Stream<T> |
(Predicate<T> predicate) |
T -> boolean |
unordered (p. 932) |
Stream<T> |
() |
|