Home > Articles

This chapter is from the book

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

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

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

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

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

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

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

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

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>

()

 

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020