16.3 Stream Basics

In this section we introduce the terminology and the basic concepts required to work with streams, and provide an overview of the Stream API.

An example of an aggregate operation is filtering the elements in the stream according to some criteria, where all the elements of the stream must be examined in order to determine the result. We saw a simple example of filtering a list in the previous section. Figure 16.1a shows an example of a stream-based solution for filtering a list of CDs to find all pop music CDs. A stream of CDs is created at (1). The stream is filtered at (2) to find pop music CDs. Each pop music CD that is found is accumulated into a result list at (3). Note how the method calls are chained, which is typical of processing elements in a stream. We will fill in the details as we use Figure 16.1 to introduce the basics of data processing using streams.

Figure 16.1 Data Processing with Streams

A stream must be built from a data source before operations can be performed on its elements. Streams come in two flavors: those that process object references, called object streams; and those that process numeric values, called numeric streams. In Figure 16.1a, the call to the stream() method on the list cdList creates an object stream of CD references with cdList as the data source. Building streams from data sources is explored in §16.4, p. 890.

Stream operations are characterized either as intermediate operations (§16.5, p. 905) or terminal operations (§16.7, p. 946). In Figure 16.1a, there are two stream operations. The methods filter() and collect() implement an intermediate operation and a terminal operation, respectively.

An intermediate operation always returns a new stream—that is, it transforms its input stream to an output stream. In Figure 16.1a, the filter() method is called at (2) on its input stream, which is the initial stream of CDs returned by the stream() method at (1). The method reference CD::isPop passed as an argument to the filter() method implements the Predicate<CD> functional interface to filter the CDs in the stream. The filter() method returns an output stream, which is a stream of pop music CDs.

A terminal operation either causes a side effect or computes a result. The method collect() at (3) implements a terminal operation that computes a result (§16.7, p. 964). This method is called on the output stream of the filter() operation, which is now the input stream of the terminal operation. Each CD that is determined to be a pop music CD by the filter() operation at (2) is accumulated into a result list by the toList() method at (3). The toList() method (p. 972) creates an empty list and accumulates elements from the input stream—in this case, CDs with pop music.

The code below splits the chain of method calls in Figure 16.1a, but produces the same result. The code explicitly shows the streams that are created and how an operation is invoked on the stream returned by the preceding operation. However, this code is a lot more verbose and not as easy to read as the code in Figure 16.1a— so much so that it is frowned upon by stream aficionados.

Click here to view code image

Stream<CD> stream1 = cdList.stream();            // (1a) Stream creation.
Stream<CD> stream2 = stream1.filter(CD::isPop);  // (2a) Intermediate operation.
List<CD> popCDs2 = stream2.toList();             // (3a) Terminal operation.

Leave a Reply

Your email address will not be published. Required fields are marked *