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.
Finite or Infinite Stream
The size of a stream can be finite or infinite depending on how the stream is created. The generate() and iterate() methods of the core stream interfaces create streams with an infinite number of elements (p. 894). Such a stream is said to be unbounded. The overloaded ints(), longs(), and doubles() methods of the java.util.Random class create streams with an effectively unlimited number of pseudorandom values (p. 900). An infinite stream must be truncated before the terminal operation is initiated; otherwise, the stream pipeline will never terminate (p. 917).
Object or Numeric Stream
The interface Stream<T> defines the contract for streams of object references—that is, object streams. The specialized interfaces IntStream, LongStream, and DoubleStream represent streams of int, long, and double values, respectively—that is, numeric streams. The various ways to create streams discussed here will always result in a stream whose element type is either a reference type or a numeric type (int, long, or double). Conversion between these stream types is discussed in §16.5, p. 934.
Table 16.1, p. 904, summarizes selected methods for building streams from various data sources.
The following static factory methods for building streams are defined in the Stream<T> class. Counterparts to these methods are also provided by the IntStream, LongStream, and DoubleStream interfaces for creating numeric streams, unless otherwise noted:
The Empty Stream
An empty stream can be obtained by calling the empty() method of the core stream interfaces. As the name implies, such a stream has no elements.
Stream<CD> cdStream = Stream.empty(); // Empty stream of CD. System.out.println("Count: " + cdStream.count()); // Count: 0 IntStream iStream = IntStream.empty(); // Empty stream of int. System.out.println("Count: " + iStream.count()); // Count: 0
The count() method is a terminal operation in the Stream<T> interface (p. 953). It returns the number of elements processed through the stream pipeline.
Using the null value to indicate that a stream is empty may result in a NullPointer-Exception. Therefore, using an explicit empty stream is highly recommended.
Streams from Specified Values
The two overloaded of() methods in the core stream interfaces create finite sequential ordered streams from data values that are specified as arguments.
In the code below, the single-argument of() method is called at (1), and the variable arity of() method is called at (2), both creating a stream of element type CD. The size of the streams created at (1) and (2) is 1 and 3, respectively. The stream pipeline comprising (3) and (4) filters the pop music CDs and prints their title at (4). The forEach() terminal operation at (4) applies its Consumer action to each pop music CD.
// From specified objects. Stream<CD> cdStream1 = Stream.of(CD.cd0); // (1) Single-arg call. Stream<CD> cdStream2 = Stream.of(CD.cd0, CD.cd1, CD.cd2); // (2) Varargs call. cdStream2.filter(CD::isPop) // (3) .forEach(cd -> System.out.println(cd.title())); // (4)
The code below shows examples of using numeric values to create streams. The values specified at (1) and (2) are autoboxed to create a stream of objects. The declaration statements at (3) and (4) avoid the overhead of autoboxing when streams of numeric values are created. However, at (4), an implicit numeric conversion to double is applied to the non-double values.
// From specified numeric values. Stream<Integer> integerStream1 = Stream.of(2017, 2018, 2019); // (1) Stream<? extends Number> numStream = Stream.of(100, 3.14D, 5050L); // (2) IntStream intStream1 = IntStream.of(2017, 2018, 2019); // (3) DoubleStream doubleStream = DoubleStream.of(100, 3.14D, 5050L); // (4)
The variable arity of() method can be used to create a stream whose source is an array. Equivalently, the overloaded Arrays.stream() method can be used for the same purpose. In all cases below, the size of the stream is the same as the size of the array, except at (7). An int array is an object that is passed to the single-argument Stream.Of() method (creating a Stream<int[]>), and not the variable arity Stream.of() method. The int array is, however, passed to the variable arity IntStream.of() method at (8). Creating a stream from a numeric array is safer with the numeric stream interfaces or the Arrays.stream() method than the Stream.of() method.
// From an array of CDs. Stream<CD> cdStream3 = Stream.of(CD.cdArray); // (1) Stream<CD> cdStream4 = Arrays.stream(CD.cdArray); // (2) // From an array of Integer. Integer[] integerArray = {2017, 2018, 2019}; // (3) Stream<Integer> integerStream2 = Stream.of(integerArray); // (4) Stream<Integer> integerStream3 = Arrays.stream(integerArray); // (5) // From an array of int. int[] intArray = {2017, 2018, 2019}; // (6) Stream<int[]> intArrayStream = Stream.of(intArray); // (7) Size is 1. IntStream intStream2 = IntStream.of(intArray); // (8) Size is 3. IntStream intStream3 = Arrays.stream(intArray); // (9) Size is 3.
The Stream.of() methods throw a NullPointerException if the argument is null. The ofNullable() method, on the other hand, returns an empty stream if this is the case; otherwise, it returns a singleton stream.
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);
Concatenating Streams
The concat() method creates a resulting stream where the elements from the first argument stream are followed by the elements from the second argument stream. The code below illustrates this operation for two unordered sequential streams. Two sets are created at (1) and (2) based on lists of strings that are passed to the set constructors. The two streams created at (3) and (4) are unordered, since they are created from sets (p. 897). These unordered streams are passed to the concat() method at (5). The resulting stream is processed in the pipeline comprising (5) and (6). The forEachOrdered() operation at (6) respects the encounter order of the stream if it has one—that is, if it is ordered (p. 948). The output confirms that the resulting stream is unordered.
Set<String> strSet1 // (1) = Set.of("All", " objects", " are", " equal"); Set<String> strSet2 // (2) = Set.of(" but", " some", " are", " more", " equal", " than", " others."); Stream<String> unorderedStream1 = strSet1.stream(); // (3) Stream<String> unorderedStream2 = strSet2.stream(); // (4) Stream.concat(unorderedStream1, unorderedStream2) // (5) .forEachOrdered(System.out::print); // (6) // objectsAll equal are some are others. than equal more but
The resulting stream is ordered if both argument streams are ordered. The code below illustrates this operation for two ordered sequential streams. The two streams created at (1) and (2) below are ordered. The ordering is given by the specification order of the strings as arguments to the Stream.of() method. These ordered streams are passed to the concat() method at (3). The resulting stream is processed in the pipeline comprising (3) and (4). The output confirms that the resulting stream is ordered.
Stream<String> orderedStream1 = Stream.of("All", " objects", // (1) " are", " equal"); Stream<String> orderedStream2 = Stream.of(" but", " some", " are", " more", // (2) " equal", " than", " others."); Stream.concat(orderedStream1, orderedStream2) // (3) .forEachOrdered(System.out::print); // (4) // All objects are equal but some are more equal than others.
As far as the mode of the resulting stream is concerned, it is parallel if at least one of the constituent streams is parallel. The code below illustrates this behavior.
The parallel() intermediate operation used at (1) returns a possibly parallel stream (p. 933). The call to the isParallel() method confirms this at (2). We pass one parallel stream and one sequential stream to the concat() method at (3). The call to the isParallel() method at (4) confirms that the resulting stream is parallel. The printout from (5) shows that it is also unordered. Note that new streams are created on the sets strSet1 and strSet2 at (1) and (3), respectively, as we cannot reuse the streams that were created earlier and consumed.
Stream<String> pStream1 = strSet1.stream().parallel(); // (1) System.out.println("pStream1 is parallel: " + pStream1.isParallel()); // (2) true Stream<String> rStream = Stream.concat(pStream1, strSet2.stream()); // (3) System.out.println("rStream is parallel: " + pStream1.isParallel()); // (4) true rStream.forEachOrdered(System.out::print); // (5) // objectsAll equal are some are others. than equal more but
Streams from Collections
The default methods stream() and parallelStream() of the Collection interface create streams with collections as the data source. Collections are the only data source that provide the parallelStream() method to create a parallel stream directly. Otherwise, the parallel() intermediate operation must be used in the stream pipeline.
The following default methods for building streams from collections are defined in the java.util.Collection interface:
We have already seen examples of creating streams from lists and sets, and several more examples can be found in the subsequent sections.
The code below illustrates two points about streams and their data sources. If the data source is modified before the terminal operation is initiated, the changes will be reflected in the stream. A stream is created at (2) with a list of CDs as the data source. Before a terminal operation is initiated on this stream at (4), an element is added to the underlying data source list at (3). Note that the list created at (1) is modifiable. The count() operation correctly reports the number of elements processed in the stream pipeline.
List<CD> listOfCDS = new ArrayList<>(List.of(CD.cd0, CD.cd1)); // (1) Stream<CD> cdStream = listOfCDS.stream(); // (2) listOfCDS.add(CD.cd2); // (3) System.out.println(cdStream.count()); // (4) 3 // System.out.println(cdStream.count()); // (5) IllegalStateException
Trying to initiate an operation on a stream whose elements have already been consumed results in a java.lang.IllegalStateException. This case is illustrated at (5). The elements in the cdStream were consumed after the terminal operation at (4). A new stream must be created on the data source before any stream operations can be run.
To create a stream on the entries in a Map, a collection view can be used. In the code below, a Map is created at (1) and populated with some entries. An entry view on the map is obtained at (2) and used as a data source at (3) to create an unordered sequential stream. The terminal operation at (4) returns the number of entries in the map.
Map<Integer, String> dataMap = new HashMap<>(); // (1) dataMap.put(1, "en"); dataMap.put(2, "to"); dataMap.put(3, "tre"); dataMap.put(4, "fire"); long numOfEntries = dataMap .entrySet() // (2) .stream() // (3) .count(); // (4) 4
In the examples in this subsection, the call to the stream() method can be replaced by a call to the parallelStream() method. The stream will then execute in parallel, without the need for any additional synchronization code (p. 1009).
Streams from Arrays
We have seen examples of creating streams from arrays when discussing the variable arity of() method of the stream interfaces and the overloaded Arrays.stream() methods earlier in the chapter (p. 893). The sequential stream created from an array has the same order as the positional order of the elements in the array. As far as numeric streams are concerned, only an int, long, or double array can act as the data source of such a stream.
The code below illustrates creating a stream based on a subarray that is given by the half-open interval specified as an argument to the Array.stream() method, as shown at (1). The stream pipeline at (2) calculates the length of the subarray.
Stream<CD> cdStream = Arrays.stream(cdArray, 1, 4); // (1) long noOfElements = cdStream.count(); // (2) 3
The following overloaded static methods for building sequential ordered streams from arrays are defined in the java.util.Arrays class:
Building a Numeric Stream with a Range
The overloaded methods range() and rangeClosed() can be used to create finite ordered streams of integer values based on a range that can be half-open or closed, respectively. The increment size is always 1.
The following static factory methods for building numeric streams are defined only in the IntStream and LongStream interfaces in the java.util.stream package.
The range(startInclusive, endExclusive) method is equivalent to the following for(;;) loop:
for (int i = startInclusive; i < endExclusive; i++) { // Loop body. }
When processing with ranges of integer values, the range() methods should also be considered on par with the for(;;) loop.
The stream pipeline below prints all the elements in the CD array in reverse. Note that no terminating condition or increment expression is specified. As range values are always in increasing order, a simple adjustment can be done to reverse their order.
IntStream.range(0, CD.cdArray.length) // (1) .forEach(i -> System.out.println(cdArray[CD.cdArray.length - 1 - i]));
The following example counts the numbers that are divisible by a specified divisor in a given range of values.
int divisor = 5; int start = 2000, end = 3000; long divisibles = IntStream .rangeClosed(start, end) // (1) .filter(number -> number % divisor == 0) // (2) .count(); // (3) System.out.println(divisibles); // 201
The next example creates an int array that is filled with increment values specified by the range at (1) below. The toArray() method is a terminal operation that creates an array of the appropriate type and populates it with the values in the stream (p. 971).
int first = 10, len = 8; int[] intArray = IntStream.range(first, first + len).toArray(); // (1) System.out.println(intArray.length + ": " + Arrays.toString(intArray)); //8: [10, 11, 12, 13, 14, 15, 16, 17]
The example below shows usage of two nested ranges to print the multiplication tables. The inner arrange is executed 10 times for each value in the outer range.
IntStream.rangeClosed(1, 10) // Outer range. .forEach(i -> IntStream.rangeClosed(1, 10) // Inner range. .forEach(j -> System.out.printf("%2d * %2d = %2d%n", i, j, i * j))); }
We cordially invite the inquisitive reader to code the above examples in the imperative style using explicit loops. Which way is better is not always that clear-cut.
Numeric Streams Using the Random Class
The following methods for building numeric unordered streams are defined in the java.util.Random class:
The examples below illustrate using a pseudorandom number generator (PRNG) to create numeric streams. The same PRNG can be used to create multiple streams. The PRNG created at (1) will be used in the examples below.
Random rng = new Random(); // (1)
The int stream created at (2) is an effectively unlimited unordered stream of int values. The size of the stream is limited to 3 by the limit() operation. However, at (3), the maximum size of the stream is specified in the argument to the ints() method. The values in both streams at (2) and (3) can be any random int values. The contents of the array constructed in the examples will, of course, vary.
IntStream iStream = rng.ints(); // (2) Unlimited, any int value int[] intArray = iStream.limit(3).toArray(); // Limits size to 3 //[-1170441471, 1070948914, 264046613] intArray = rng.ints(3).toArray(); // (3) Size 3, any int value //[1011448344, -974832344, 816809715]
The unlimited unordered stream created at (4) simulates the dice throw we implemented earlier using the generate() method (p. 895). The values are between 1 and 6, inclusive. The limit() method must be used explicitly to limit the stream. The finite unordered stream created at (5) incorporates the size and the value range.
intArray = rng.ints(1, 7) // (4) Unlimited, [1, 6] .limit(3) // Limits size to 3 .toArray(); // [5, 2, 4] intArray = rng.ints(3, 1, 7) // (5) Size 3, [1, 6] .toArray(); // [1, 4, 6]
The zero-argument doubles() method and the single-argument doubles(streamSize) method generate an unlimited and a limited unordered stream, respectively, whose values are between 0.0 and 1.0 (exclusive).
DoubleStream dStream = rng.doubles(3); // (6) Size 3, [0.0, 1.0) double[] dArray = dStream.toArray(); //[0.9333794789872794, 0.7037326827186609, 0.2839257522887708]
Streams from a CharSequence
The CharSequence.chars() method creates a finite sequential ordered IntStream from a sequence of char values. The IntStream must be transformed to a Stream<Character> in order to handle the values as Characters. The IntStream.mapToObj() method can be used for this purpose, as shown at (2). A cast is necessary at (2) in order to convert an int value to a char value which is autoboxed in a Character. Conversion between streams is discussed in §16.5, p. 934.
String strSource = "banananana"; IntStream iStream = strSource.chars(); // (1) iStream.forEach(i -> System.out.print(i + " ")); // Prints ints. // 98 97 110 97 110 97 110 97 110 97 strSource.chars() .mapToObj(i -> (char)i) // (2) Stream<Character> .forEach(System.out::print); // Prints chars. // banananana
The following default method for building IntStreams from a sequence of char values (e.g., String and StringBuilder) is defined in the java.lang.CharSequence interface (§8.4, p. 444):
Streams from a String
The following method of the String class can be used to extract text lines from a string:
In the code below, the string at (1) contains three text lines separated by the line terminator (\n). A stream of element type String is created using this string as the source at (2). Each line containing the word "mistakes" in this stream is printed at (3).
String inputLines = "Wise men learn from their mistakes.\n" // (1) + "But wiser men learn from the mistakes of others.\n" + "And fools just carry on."; Stream<String> lStream = inputLines.lines(); // (2) lStream.filter(l -> l.contains("mistakes")).forEach(System.out::println); // (3)
Output from the code:
Wise men learn from their mistakes. But wiser men learn from the mistakes of others.
Streams from a BufferedReader
A BufferedReader allows contents of a text file to be read as lines. A line is a sequence of characters terminated by a line terminator sequence. Details of using a Buffered-Reader are covered in §20.3, p. 1251. A simple example of creating streams on text files using a BufferedReader is presented below.
At (1) and (2) in the header of the try-with-resources statement (§7.7, p. 407), a BufferedReader is created to read lines from a given file, and a stream of String is created at (3) by the lines() method provided by the BufferedReader class. These declarations are permissible since both the buffered reader and the stream are Auto-Closeable. Both will be automatically closed after the try block completes execution. A terminal operation is initiated at (4) on this stream to count the number of lines in the file. Of course, each line from the stream can be processed depending on the problem at hand.
try ( FileReader fReader = new FileReader("CD_Data.txt"); // (1) BufferedReader bReader = new BufferedReader(fReader); // (2) Stream<String> lStream = bReader.lines() ) { // (3) System.out.println("Number of lines: " + lStream.count()); // (4) 13 } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
The following method for building a Stream<String> from a text file is defined in the java.io.BufferedReader class:
Streams from Factory Methods in the Files Class
A detailed discussion of the NIO2 File API that provides the classes for creating the various streams for reading files, finding files, and walking directories in the file system can be found in Chapter 21, p. 1285.
Analogous to the lines() method in the BufferedReader class, a static method with the same name is provided by the java.nio.file.Files class that creates a stream for reading the file content as lines.
In the example below, a Path is created at (1) to represent a file on the file system. A stream is created to read lines from the path at (2) in the header of the try-with-resources statement (§7.7, p. 407). As streams are AutoCloseable, such a stream is automatically closed after the try block completes execution. As no character set is specified, bytes from the file are decoded into characters using the UTF-8 charset. A terminal operation is initiated at (3) on this stream to count the number of lines in the file. Again, each line in the stream can be processed as desired.
Path path = Paths.get("CD_Data.txt"); // (1) try (Stream<String> lStream = Files.lines(path)) { // (2) System.out.println("Number of lines: " + lStream.count()); // (3) 13 } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
The following static methods for building a Stream<String> from a text file are defined in the java.nio.file.Files class:
Summary of Stream Building Methods
Selected methods for building streams from various data sources are listed in Table 16.1. The first column lists the method names and the reference type that provides them. For brevity, the parameters of the methods are omitted. Note that some methods are overloaded. The prefix NumType stands for Int, Long, or Double. A reference is also provided where details about the method can be found. The remaining four columns indicate various aspects of a stream: the type of stream returned by a method, whether the stream is finite or infinite, whether it is sequential or parallel, and whether it is ordered or unordered (p. 891).
Table 16.1 Summary of Stream Building Methods
Method |
Returned stream type |
Finite/Infinite |
Sequential/Parallel |
Ordered/Unordered |
---|---|---|---|---|
Stream.empty() NumTypeStream.empty() (p. 893) |
Stream<T> NumTypeStream |
Finite |
Sequential |
Ordered |
Stream.of() Stream.ofNullable() NumTypeStream.of() (p. 893) |
Stream<T> Stream<T> NumTypeStream |
Finite |
Sequential |
Ordered |
Stream.generate() NumTypeStream.generate() (p. 895) |
Stream<T> NumTypeStream |
Infinite |
Sequential |
Unordered |
Stream.iterate() NumTypeStream.iterate (p. 895) |
Stream<T> NumTypeStream |
Infinite |
Sequential |
Ordered |
Stream.concat() NumTypeStream.concat() (p. 895) |
Stream<T> NumTypeStream |
Finite if both finite |
Parallel if either parallel |
Ordered if both ordered |
Collection.stream() (p. 897) |
Stream<T> |
Finite |
Sequential |
Ordered if collection ordered |
Collection.parallelStream() (p. 897) |
Stream<T> |
Finite |
Parallel |
Ordered if collection ordered |
Arrays.stream() (p. 898) |
Stream<T> NumTypeStream |
Finite |
Sequential |
Ordered |
IntStream.range() IntStream.rangeClosed() LongStream.range() LongStream.rangeClosed() (p. 898) |
IntStream IntStream LongStream LongStream |
Finite |
Sequential |
Ordered |
Random.ints() Random.longs() Random.doubles() (p. 900) |
IntStream LongStream DoubleStream |
Finite or infinite, depending on parameters |
Sequential |
Unordered |
CharSequence.chars() (p. 901) |
IntStream |
Finite |
Sequential |
Ordered |
String.lines() (p. 902) |
Stream<String> |
Finite |
Sequential |
Ordered |
BufferedReader.lines() (p. 902) |
Stream<String> |
Finite |
Sequential |
Ordered |
Files.lines() (p. 903) |
Stream<String> |
Finite |
Sequential |
Ordered |