Building Streams – Streams
16.4 Building Streams
A stream must have a data source. In this section we will explore how streams can be created from various data sources: collections, arrays, specified values, generator functions, strings, and I/O channels, among others.
Aspects to Consider When Creating Streams
When creating a stream from a data source, certain aspects to consider include whether the stream is:
- Sequential or parallel
- Ordered or unordered
- Finite or infinite
- Object or numeric
Sequential or Parallel Stream
A sequential stream is one whose elements are processed sequentially (as in a for loop) when the stream pipeline is executed by a single thread. Figure 16.1 illustrates the execution of a sequential stream, where the stream pipeline is executed by a single thread.
A parallel stream is split into multiple substreams that are processed in parallel by multiple instances of the stream pipeline being executed by multiple threads, and their intermediate results combined to create the final result. Parallel streams are discussed in detail later (p. 1009).
The different ways to create a stream on a data source that are illustrated in this section result in a sequential stream. A parallel stream can only be created directly on a collection by invoking the Collection.parallelStream() method (p. 897).
The sequential or parallel mode of an existing stream can be modified by calling the BaseStream.sequential() and BaseStream.parallel() intermediate operations, respectively (p. 933). A stream is executed sequentially or in parallel depending on the execution mode of the stream on which the terminal operation is initiated.
Ordered or Unordered Stream
The encounter order of a stream refers to the way in which a stream makes its elements available for processing to an operation in a pipeline. For such data sources as a list, the encounter order of the initial stream is the same as the order of the elements in the list, whereas a stream created with a set of values does not have an encounter order, as the elements of a set are considered to be unordered.
The encounter order of a stream may be changed by an intermediate operation. For example, the sorted() operation may impose an encounter order on an unordered stream (p. 929), and the unordered() operation may designate a stream unordered (p. 932). Also, some terminal operations might choose to ignore the encounter order; an example is the forEach() operation (p. 948).
For ordered sequential streams, an identical result is produced when identical stream pipelines are executed on an identical data source—that is, the execution is deterministic. This guarantee does not hold for unordered sequential streams, as the results produced might be different.
Processing of unordered parallel streams may have better performance than for ordered parallel streams in identical stream pipelines when the ordering constraint is removed, as maintaining the order might carry a performance penalty.
Mapping a Numeric Stream to an Object Stream – Streams
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]
Converting between Stream Types – Streams
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
Summary of Intermediate Stream Operations – Streams
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> | () |
Mapping between Numeric Streams – Streams
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
The Optional Class – Streams
16.6 The Optional Class
When a method returns a null value, it is not always clear whether the null value represents a valid value or the absence of a value. Methods that can return null values invariably force their callers to check the returned value explicitly in order to avoid a NullPointerException before using the returned value. For example, method chaining, which we have seen for composing stream pipelines, becomes cumbersome if each method call must be checked to see whether it returns a null value before calling the next method, resulting in a cascade of conditional statements.
The concept of an Optional object allows the absence of a value to be handled in a systematic way, making the code robust by enforcing that a consumer of an Optional must also deal with the case when the value is absent. Taking full advantage of Optional wrappers requires using them the right way, primarily to handle situations where the value returned by a method is absent.
The generic class Optional<T> provides a wrapper that represents either the presence or absence of a non-null value of type T. In other words, the wrapper either contains a non-null value of type T or no value at all.
Example 16.8 illustrates using objects of the Optional<T> class.
Declaring and Returning an Optional
Example 16.8 illustrates declaring and returning an Optional. A book is represented by the Book class that has an optional blurb of type String; that is, a book may or may not have a blurb. The Optional<T> class is parameterized with the type String in the declaration, and so is the return type of the method that returns the optional blurb.
class Book {
private Optional<String> optBlurb;
public Optional<String> getOptBlurb() { return optBlurb; }
//…
}
Execution Mode of a Stream – Streams
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.
Stream<T> parallel()
Stream<T> sequential()
Set the execution mode of a stream. They return a parallel or a sequential stream that has the same elements as this stream, respectively. Each method will return itself if this stream is already parallel or sequential, respectively.
These are intermediate operations that do not change the stream size, the stream element type, or the encounter order.
boolean isParallel()
Returns whether this stream would execute in parallel when the terminal operation is invoked. The method might yield unpredictable results if called after a terminal operation has been invoked.
It is not an intermediate operation.
Filtering – Streams
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.
Stream<T> filter(Predicate<? super T> predicate)
Returns a stream consisting of the elements of this stream that match the given non-interfering, stateless predicate.
This is a stateless intermediate operation that changes the stream size, but not the stream element type or the encounter order of the stream.
// Taking and dropping elements using a predicate.
default Stream<T> takeWhile(Predicate<? super T> predicate)
default Stream<T> dropWhile(Predicate<? super T> predicate)
The takeWhile() method puts an element from the input stream into its output stream, if it matches the predicate—that is, if the predicate returns the value true for this element. In this case, we say that the takeWhile() method takes the element.
The dropWhile() method discards an element from its input stream, if it matches the predicate—that is, if the predicate returns the value true for this element. In this case, we say that the dropWhile() method drops the element.
For an ordered stream:
The takeWhile() method takes elements from the input stream as long as an element matches the predicate, after which it short-circuits the stream processing.
The dropWhile() method drops elements from the input stream as long as an element matches the predicate, after which it passes through the remaining elements to the output stream.
In short, both methods find the longest prefix of elements to take or drop from the input stream, respectively.
For an unordered stream, where the predicate matches some but not all elements in the input stream:
The elements taken by the takeWhile() method or dropped by the dropWhile() method are nondeterministic; that is, any subset of matching elements can be taken or dropped, respectively, including the empty set.
If the predicate matches all elements in the input stream, regardless of whether the stream is ordered or unordered:
The takeWhile() method takes all elements; that is, the result is the same as the input stream.
The dropWhile() method drops all elements; that is, the result is the empty stream.
If the predicate matches no elements in the input stream, regardless of whether the stream is ordered or unordered:
The takeWhile() method takes no elements; that is, the result is the empty stream.
The dropWhile() method drops no elements; that is, the result is the same as the input stream.
Note that the takeWhile() method is a short-circuiting stateful intermediate operation, whereas the dropWhile() method is a stateful intermediate operation.
// Selecting distinct elements.
Stream<T> distinct()
Returns a stream consisting of the distinct elements of this stream, where no two elements are equal according to the Object.equals() method; that is, the method assumes that the elements override the Object.equals() method. It also uses the hashCode() method to keep track of the elements, and this method should also be overridden from the Object class.
For ordered streams, the first occurrence of a duplicated element is selected in the encounter order—called the stability guarantee. This stateful operation is particularly expensive for a parallel ordered stream which entails buffering overhead to ensure the stability guarantee. There is no such guarantee for an unordered stream: Which occurrence of a duplicated element will be selected is not guaranteed.
This stateful intermediate operation changes the stream size, but not the stream element type.
// Skipping elements.
Stream<T> skip(long n)
Returns a stream consisting of the remaining elements of this stream after discarding the first n elements of the stream in encounter order. If this stream has fewer than n elements, an empty stream is returned.
This stateful operation is expensive for a parallel ordered stream which entails keeping track of skipping the first n elements.
This is a stateful intermediate operation that changes the stream size, but not the stream element type.
// Truncating a stream.
Stream<T> limit(long maxSize)
Returns a stream consisting of elements from this stream, truncating the length of the returned stream to be no longer than the value of the parameter maxSize.
This stateful operation is expensive for a parallel ordered stream which entails keeping track of passing the first n elements from the input stream to the output stream.
This is a short-circuiting, stateful intermediate operation.
Using Generator Functions to Build Infinite Streams – Streams
Using Generator Functions to Build Infinite Streams
The generate() and iterate() methods of the core stream interfaces can be used to create infinite sequential streams that are unordered or ordered, respectively.
Infinite streams need to be truncated explicitly in order for the terminal operation to complete execution, or the operation will not terminate. Some stateful intermediate operations must process all elements of the streams in order to produce their results—for example, the sort() intermediate operation (p. 929) and the reduce() terminal operation (p. 955). The limit(maxSize) intermediate operation can be used to limit the number of elements that are available for processing from a stream (p. 917).
Generate
The generate() method accepts a supplier that generates the elements of the infinite stream.
IntSupplier supplier = () -> (int) (6.0 * Math.random()) + 1; // (1)
IntStream diceStream = IntStream.generate(supplier); // (2)
diceStream.limit(5) // (3)
.forEach(i -> System.out.print(i + ” “)); // (4) 2 4 5 2 6
The IntSupplier at (1) generates a number between 1 and 6 to simulate a dice throw every time it is executed. The supplier is passed to the generate() method at (2) to create an infinite unordered IntStream whose values simulate throwing a dice. In the pipeline comprising (3) and (4), the number of values in the IntStream is limited to 5 at (3) by the limit() intermediate operation, and the value of each dice throw is printed by the forEach() terminal operation at (4). We can expect five values between 1 and 6 to be printed when the pipeline is executed.
Iterate
The iterate() method accepts a seed value and a unary operator. The method generates the elements of the infinite ordered stream iteratively: It applies the operator to the previous element to generate the next element, where the first element is the seed value.
In the code below, the seed value of 1 is passed to the iterate() method at (2), together with the unary operator uop defined at (1) that increments its argument by 2. The first element is 1 and the second element is the result of the unary operator applied to 1, and so on. The limit() operation limits the stream to five values. We can expect the forEach() operation to print the first five odd numbers.
IntUnaryOperator uop = n -> n + 2; // (1)
IntStream oddNums = IntStream.iterate(1, uop); // (2)
oddNums.limit(5)
.forEach(i -> System.out.print(i + ” “)); // 1 3 5 7 9
The following stream pipeline will really go bananas if the stream is not truncated by the limit() operation:
Stream.iterate(“ba”, b -> b + “na”)
.limit(5)
.forEach(System.out::println);
Lazy Execution – Streams
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.
Archives
- July 2024
- June 2024
- May 2024
- March 2024
- February 2024
- January 2024
- December 2023
- October 2023
- September 2023
- May 2023
- March 2023
- January 2023
- December 2022
- November 2022
- October 2022
- September 2022
- August 2022
- July 2022
- April 2022
- March 2022
- November 2021
- October 2021
- September 2021
- July 2021
- June 2021
- March 2021
- February 2021
Calendar
M | T | W | T | F | S | S |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 |