Java Reference
In-Depth Information
into
Files.exists
, and then pass the satisfying
Path
instances into our readLines lambda to generate
our
String
instances. Our inputs are String instances and our outputs are
String
instances, but we are
performing processing and logic on the
Path
instances that exist only within the stream itself.
Listing 3-12.
Creating a stream of all the lines from existing files given a list of file names via Stream.filter
Function<Path, Stream<String>> readLines = path -> {
try {
return Files.lines(path);
} catch (IOException ioe) {
throw new RuntimeException("Error reading " + path, ioe);
}
};
Stream<String> lines = Stream.of("foo.txt", "bar.txt", "baz.txt")
.map(Paths::get)
.filter(Files::exists)
.flatMap(readLines);
Collecting, Processing, or Reducing Streams
The mapping and filtering operations that we just discussed are the intermediary operations; you can
continue to chain them together to your heart's content. Sooner or later, though, you will want to stop
massaging your stream and actually do something with it. This is where the terminal operations come in.
You only get one terminal operation per stream, and the stream is actually executed when the terminal
operation is applied. There are three basic ways to terminate a stream: by collecting its elements, by
generating side effects based on its elements, or by performing a calculation on its elements. These three
ways are referred to as
collection
,
processing
, and
reducing
, respectively.
To collect streams, you need to specify what kind of collection you want as a result, and how you want
to collect the elements. The Java SDK ships with many options in the
Collectors
class. This class produces
various kinds of
Collector
instances. A
Collector
instance is responsible for consuming elements of a
stream, updating some internal state based on those elements, and generating a result at the end. The
built-in collectors are truly impressive: you can get the average, count the number of elements, get the min
or max based on some
Comparator
, group elements based on some key (returning a
Map
of
List
instances),
concatenate the string representation of all the results into a single
String
(optionally with some delimiter),
and more. In Listing 3-13, we could go from our
Library
instance's books to a
Map
sorting those books by
genre. Note that we use the
groupingByConcurrent
method in order to maintain the permission to execute
in parallel. This is one of those moments where comparing the Stream API code to more traditional Java
code truly exposes the power of functional programming.
Listing 3-13.
Grouping Books by Genre Using Streams
Map<Book.Genre, List<Book>> booksByGenre =
library.getBooks().parallelStream()
.collect(Collectors.groupingByConcurrent(Book::getGenre));
Sometimes you do not want to collect together the results, but you just want to generate some side effect.
The classic example would be printing out the results of a stream execution. To do this, you use the
Stream.forEach
method, just as you would do on a
Collection
. The one major difference is that the
Stream.forEach
method makes no guarantees about the order of execution: a parallel stream may well
generate results in any order it chooses. If you really want to ensure ordering, you need to use the