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
 
Search WWH ::




Custom Search