Java streams 1. Intro

To make it clear, by “Java streams” I mean Java 8 streams, not reactive streams I discuss in reactive programming series. The difference is that Java 8 streams are pull-based. They are basically iterators.

The reactive streams, on the other hand, are push-based, so the subscriber (called “observer” in the reactive world) waits for the publisher (called “observable”) to emit a value the observer can process. If the publisher emits a new value before the observer had processed the current one, such an arrangement may create a problem that can be solved using various means: caching, dropping, or consolidating the values that cannot be currently processed or backpressuring the source to slow down, for example. We will talk about in reactive programming series.

Don’t get me wrong, I love Java 8 streams and use them all the time. I also love reactive streams and use them when asynchronous processing is possible and provides an advantage over the synchronized one. But it requires using a framework that implements Reactive Stream specification. The Java 8 streams, by contrast, can be used any time one uses Java 8+.

That difference established, let us talk about Java 8 streams only.

Let us start with a quick demonstration. We create a list of strings that we use as a source. Since Java 8, every Java collection interface has a default method stream() that creates a Stream object that emits (in response to the pull of the “terminal operation”) the values, one at each pull, after the previous value is processed. Here is an example:

List<String> list = List.of("it"," ","wor","ks");
list.stream().forEach(System.out::print); //prints: it works

In this example, forEach(Consumer<String>) is a terminal operation (method) that receives a lambda expression s -> System.out.print(s) in “method reference” format System.out::print. This operation applies the specified function to each of the elements it pulls from the stream. The same way a regular for statement applies a block of code to each value produced by the first line of the statement.

The operation is called terminal because it produces the final result. It terminates the stream’s elements flow.

Another kind of stream operations (methods) is called intermediate. An intermediate operation modifies or filters the stream’s elements and returns Stream object, so another operation can be called and applied on the result of the intermediate operation.

For example, the operation map(Function<String, R) converts each string to the type R, while the operation filter(Predicate<String>) allows only those elements to flow that evaluate to true by the specified Predicate<String> expression:

List<String> list = List.of("it"," ","wor","ks");
list.stream().map(s -> s.length())
             .filter(i -> i == 2)
             .forEach(System.out::print);   //prints: 22

As you can see, each string was converted to int (its length), and only those int values were allowed to flow that was equal to 2.

Since each intermediate operation returns a Stream object, it allows building a processing pipeline using the fluent dot-style, which makes the code more compact and easy to follow.

Please, note the significant difference between collections and streams. Every collection element is computed before being added to the collection, while an element emitted by a stream is not contained in the stream – it exists in the source and is computed on demand.

See other posts on Java 8 streams and posts on other topics.
You can also use navigation pages for Java stream related blogs:
— Java 8 streams blog titles
— Create stream
— Stream operations
— Stream operation collect()
The source code of all the code examples is here in GitHub

, , , ,

Send your comments using the link Contact or in response to my newsletter.
If you do not receive the newsletter, subscribe via link Subscribe under Contact.

Powered by WordPress. Designed by Woo Themes