Saturday, August 26, 2017

Java 8 Streams

Java 8 Streams API provide a declarative way to process sequence of data elements. A stream can be a sequence of elements from a collection, a I/O channel or a generator function. To process a stream, a set of operations are composed into a pipeline. A stream pipeline consists of a Source (data stream), one or more intermediate operations (apply transformations on the stream) and a terminal operation that produces a result or a side-effect. The following example (from the Stream javadoc), has one source (widgets), two intermediate operations (filter and mapToInt) and a terminal operation (sum).
  int sum = widgets.stream() // Source
                    .filter(w -> w.getColor() == RED) // Intermediate operation
                    .mapToInt(w -> w.getWeight())  //Intermediate operation
                    .sum();  // Terminal operation, produces result
In this article, we will go over the basics of streams and how to create or initialize the source for a stream pipeline.

Collections and Streams

Although Collections and Streams appear similar at a high level, they are more different than they are similar.
  • All the different types of collections are in-memory data structures used to store data elements, while streams do not hold any data, but compute the data on-demand.
  • Processing collections in parallel requires writing parallel code, but Streams provide declarative way to process data in parallel.
  • Collections provide direct access to the data elements, while streams do not provide direct access to the elements (although there are a couple of ways provided to work around this limitation, it is not the main purpose of streams)

Stream Operations

Stream operations can be categorized into intermediate or terminal operations. Intermediate operations produce a stream as a result and can be chained in the pipeline while terminal operations either produce a non-stream result or have a side-effect, and as such cannot be chained. Regardless of the type of operation, most Stream operations take a Lambda expression as a parameter which describes the action taken on the data elements of the Stream. Creating Streams There are multiple ways to create Streams. The simplest way is to use the Stream.of() method as shown below.
Stream<String> streamOfString = Stream.of("create", "a", "basic", "stream");
More commonly, you would see streams created from collections or files etc. as shown below.
// Create stream from a list
List<string> stringList = Arrays.asList("create", "a", "basic", "stream");
stringList.stream().forEach(System.out::println);
// Print lines in a file using streams
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file"), Charset.defaultCharset())) {
	lines.forEach(System.out::println);
} catch (IOException e) {
	e.printStackTrace();
}

Generating Streams Dynamically

Java 8 provides a couple ways to generate streams dynamically using generator functions, iterate(T seed, UnaryOperator<T> f) and generate(Supplier<T> s) as shown below. Both these generators can each create infinite sequence of elements, which is the reason to use the limit() method to limit the number of elements being processed
Stream<Integer> intStream = Stream.iterate(0, n -> n+1).limit(10);
intStream.forEach(System.out::println);
Stream<Double> doubleStream = Stream.generate(Math::random).limit(10);
doubleStream.forEach(System.out::println);

No comments:

Post a Comment

Popular Posts