Java Stream API - BEHIND JAVA

Java Stream API

Share This

Stream is one of the major new functionality in Java 8. The whole idea of streams is to enable functional-style operations on streams of elements. A stream is an abstraction, it’s not a data structure. It’s not a collection where you can store elements. The most important difference between a stream and a structure is that a stream doesn’t hold the data. For example you cannot point to a location in the stream where a certain element exists. You can only specify the functions that operate on that data. A stream is an abstraction of a non-mutable collection of functions applied in some order to the data.

Stream and collection - Comparison

Streams differ from collections in several ways:

  • No storage. A stream is not a data structure that stores elements; instead, it conveys elements from a source such as a data structure, an array, a generator function, or an I/O channel, through a pipeline of computational operations.
  • Functional in nature. An operation on a stream produces a result, but does not modify its source. For example, filtering a Stream obtained from a collection produces a new Stream without the filtered elements, rather than removing elements from the source collection.
  • Laziness-seeking. Many stream operations, such as filtering, mapping, or duplicate removal, can be implemented lazily, exposing opportunities for optimization. For example, "find the first String with three consecutive vowels" need not examine all the input strings. Stream operations are divided into intermediate (Stream-producing) operations and terminal (value- or side-effect-producing) operations. Intermediate operations are always lazy.
  • Possibly unbounded. While collections have a finite size, streams need not. Short-circuiting operations such as limit(n) or findFirst() can allow computations on infinite streams to complete in finite time.
  • Consumable. The elements of a stream are only visited once during the life of a stream. Like an Iterator, a new stream must be generated to revisit the same elements of the source.

You obtain a stream from a collection like this:

List items = new ArrayList();

items.add("one");
items.add("two");
items.add("three");

Stream stream = items.stream();

First a List of strings is created and three strings are added to it. Then a Stream of strings is obtained by calling the items.stream() method. This is similar to how you obtain an Iterator by calling the items.iterator() method, but a Stream is entirely different than an Iterator.

Once you have obtained a Stream instance from a Collection instance, you use that stream to process the elements in the collection.

Processing the elements in the stream happens in two steps / phases:

  • Configuration
  • Processing

First the stream is configured. The configuration can consist of filters and mappings. Second, the stream is processed. The processing consists of doing something to the filtered and mapped objects. No processing takes place during the configuring calls. Not until a processing method is called on the stream.

Stream Operations

Stream.filter()

You filter a stream using the filter() method. Here is a stream filtering example:

    stream.filter( item -> item.startsWith("o") );

The complete Example looks like follows

The filter() method takes a Predicate as parameter. The Predicate interface contains a function called test() which the lambda expression passed as parameter above is matched against. In other words, the lambda expression implements the Predicate.test() method.

The test() method is defined like this:

boolean test(T t)

The parameter passed to the filter() function determines what items in the stream should be processed, and which that should be excluded from the processing. If the Predicate.test() method of the parameter passed to filter() returns true for an item, that means it should be processed. If false is returned, the item is not processed.

Here is the alternate implementation for the above example. Here we used annonymous inner class insteadof lambda expression

Stream.map()

It is possible to map the items in a collection to other objects. In other words, for each item in the collection you create a new object based on that item. How the mapping is done is up to you. Here is a simple Java stream mapping example:

items.stream()
     .map( item -> item.toUpperCase() )

This example maps all strings in the items collection to their uppercase equivalents.

Again, this example doesn't actually perform the mapping. It only configures the stream for mapping. Once one of the stream processing methods are invoked, the mapping (and filtering) will be performed.

Stream.collect()

The collect() method is one of the stream processing methods on the Stream interface. When this method is invoked, the filtering and mapping will take place and the object resulting from those actions will be collected. Here is a stream.collect() example:

List filtered = items.stream()
    .filter( item -> item.startsWith("o") )
    .collect(Collectors.toList());

This example creates a stream, adds a filter, and collects all object accepted by the filter in a List. The filter only accepts items (strings) which start with the character o. The resulting List thus contains all strings from the items collection which starts with the character o.

Stream.reduce()

reduce operation applies a binary operator to each element in the stream where the first argument to the operator is the return value of the previous application and second argument is the current stream element.

The reduce() method can reduce the elements of a stream to a single value. Here is an example:

String reduced2 = items.stream()
        .reduce((acc, item) -> acc + " " + item)
        .get();

This method takes two parameters. The acc which is the accumulated value, and item which is an element from the stream. Thus, the value created by the reduce() function is the accumulated value after processing the last element in the stream.

There is another reduce() method which takes two parameters. It takes an initial value for the accumulated value, and then a BinaryOperator. Here is an example:

String reduced = items.stream()
        .reduce("", (acc, item) -> acc + " " + item);

This example takes an empty string as initial value, and then the same lambda expression as the previous example. This version of the reduce() method returns the accumulated value directly, and not an Optional. If the stream contains no elements, the initial value will be returned.

The reduce() method can be combined with the filter() method too. Here is an example:

String reduced = items.stream()
    .filter( item -> item.startsWith("o"))
    .reduce("", (acc, item) -> acc + " " + item);

This example keeps all elements that start with the character o, and then reduce these elements into a single value.

No comments:

Post a Comment

Pages