Core Java

Become a Master of Java Streams – Part 1: Creating Streams

Declarative code (e.g. functional composition with Streams) provides superior code metrics in many cases. Code your way through this hands-on-lab article series and mature into a better Java programmer by becoming a Master of Java Streams.

The whole idea with Streams is to represent a pipeline through which data will flow and the pipeline’s functions operate on the data. This way, functional-style operations on Streams of elements can be expressed. This article is the first out of five where you will learn firsthand how to become a Master of Streams. We start with basic stream examples and progress with more complex tasks until you know how to connect standard Java Streams to databases in the Cloud.

The whole idea with Streams is to represent a pipeline through which data will flow and the pipeline’s functions operate on the data. This way, functional-style operations on Streams of elements can be expressed. This article is the first out of five where you will learn firsthand how to become a Master of Streams. We start with basic stream examples and progress with more complex tasks until you know how to connect standard Java Streams to databases in the Cloud.

Once you have completed all five articles, you will be able to drastically reduce your codebase and know how to write pure Java code for the entire applications in a blink.

Here is a summary of the upcoming articles:

Since we are firm believers in the concept of ”Learning by doing”, the series is complemented by a GitHub repository that contains Stream exercises split into 5 Units – each corresponding to the topic of an article. Instructions on how to use the source code are provided in the README-file.

What are Java Streams?

The Java Stream interface was first introduced in Java 8 and, together with lambdas, acts as a milestone in the development of Java since it contributes greatly to facilitating a declarative (functional) programming style. If you want to learn more about the advantages of declarative coding we refer you to this article.

A Java Stream can be visualized as a pipeline through which data will flow (see the image below). The pipeline’s functions will operate on the data by e.g. filtering, mapping and sorting the items. Lastly, a terminal operation can be performed to collect the items in a preferred data structure such as a
List, an Array or a Map. An important thing to notice is that a Stream can only be consumed once.

A Stream Pipeline contains three main parts; the stream source, the intermediate operation(s) (zero to many) and a terminal operation.

Let’s have a look at an example to get a glimpse of what we will be teaching throughout this series. We encourage you to look at the code below and try to figure out what the print-statement will result in before reading the next paragraph.

List <String> list = Stream.of("Monkey", "Lion", "Giraffe","Lemur")
    .filter(s -> s.startsWith("L"))
    .map(String::toUpperCase)
    .sorted()
    .collect(toList());
System.out.println(list);

Since the Stream API is descriptive and most often intuitive to use, you will probably have a pretty good understanding of the meaning of these operations regardless if you have encountered them before or not. We start off with a Stream of a List containing four Strings, each representing an African animal. The operations then filter out the elements that start with the letter “L”, converts the remaining elements to uppercase letters, sorts them in natural order (which in this case means alphabetical order) and lastly collects them into a List. Hence, resulting in the output [“LEMUR”, “LION”].

It is important to understand that Streams are “lazy” in the sense that elements are “requested” by the terminal operation (in this case the
.collect() statement). If the terminal operation only needs one element (like, for example, the terminal operation .findFirst()), then at most one element is ever going to reach the terminal operation and the reminding elements (if any) will never be produced by the source. This also means that just creating a Stream is often a cheap operation whereas consuming it might be expensive depending on the stream pipeline and the number of potential elements in the stream.

In this case, the Stream Source was a List although many other types can act as a data source. We will spend the rest of this article describing some of the most useful source alternatives.

Stream Sources

Streams are mainly suited for handling collections of objects and can operate on elements of any type T. Although, there exist three special Stream implementations; IntStream, LongStream, and DoubleStream which are restricted to handle the corresponding primitive types.

An empty Stream of any of these types can be generated by calling Stream.empty() in the following manner:

Stream <T>     Stream.empty()
IntStream  IntStream.empty()
LongStream  LongStream.empty()
DoubleStream  DoubleStream.empty()

Empty Streams are indeed handy in some cases, but the majority of the time we are interested in filling our Stream with elements. This can be accomplished in a large number of ways. We will start by looking at the special case of an IntStream since it provides a variety of useful methods.

Useful IntStreams

A basic case is generating a Stream over a small number of items. This can be accomplished by listing the integers using IntStream.of(). The code below yields a simple stream of elements 1, 2 and 3.

IntStream oneTwoThree = IntStream.of(1, 2, 3);

Listing all elements manually can be tedious if the number of items grows large. In the case where we are interested in values in a certain range, the command .rangeClosed() is more effective. The operation is inclusive, meaning that the following code will produce a stream of all elements from 1 to 9.

1
IntStream positiveSingleDigits = IntStream.rangeClosed(1, 9);

An even more powerful command is .iterate() which enables greater flexibility in terms of what numbers to include. Below, we show an example of how it can be used to produce a Stream of all numbers that are powers of two.

1
IntStream powersOfTwo = IntStream.iterate(1, i -> i * 2);

There are also several perhaps more unexpected ways of producing a Stream. The method chars() can be used to Stream over the characters in a
String, in this case, the elements “A”, “B” and “C”.

1
IntStream chars = "ABC".chars();

There is also a simple way to generate a Stream of random integers.

1
IntStream randomInts = new Random().ints();

Stream an Array

Streaming existing data collections is another option. We can stream the elements of an existing Array or choose to list items manually using Stream.of() as previously shown and repeated below.

String[] array = {"Monkey", "Lion", "Giraffe", "Lemur"};
Stream <String> stream2 = Stream.of(array);
Stream <String> stream = Stream.of("Monkey", "Lion", "Giraffe", "Lemur");

Stream from a Collection

It is also very simple to stream any Collection. The examples below demonstrate how a List or Set can be streamed with the simple command
.stream().

List <String> list = Arrays.asList("Monkey", "Lion", "Giraffe", "Lemur");
Stream <String> streamFromList = list.stream();
Set set = new HashSet<>(list);
Stream <String> streamFromSet = set.stream();

Stream from a Text File

Sometimes it can also be useful to stream the contents of a text-file. The following command will provide a Stream that holds every line from the referenced file as a separate element.

Stream <String> lines = Files.lines(Paths.get("file.txt"));

Exercise

Now that we have familiarized you with some of the ways of creating a Stream, we encourage you to clone this GitHub repo and start practicing. The content of the article will be enough to solve the first Unit which is called Create. The Unit1Create interface contains JavaDocs which describes the intended implementation of the methods in Unit1MyCreate.

public interface Unit1Create {
 /**
  * Creates a new Stream of String objects that contains
  * the elements "A", "B" and "C" in order.
  *
  * @return a new Stream of String objects that contains
  *   the elements "A", "B" and "C" in order
  */
  Stream <String> newStreamOfAToC();

The provided tests (e.g. Unit1MyCreateTest) will act as an automatic grading tool, letting you know if you solution was correct or not.

 If you have not done so yet, go ahead and solve the work items in the Unit1MyCreate class. “Gotta catch ‘em all”.

In the next article, we will continue to describe several intermediate operations that can be applied to these Streams and that will convert them into other Streams. See you soon!

Published on Java Code Geeks with permission by Per Minborg, partner at our JCG program. See the original article here: Become a Master of Java Streams – Part 1: Creating Streams

Opinions expressed by Java Code Geeks contributors are their own.

Per Minborg

I am a guy living in Palo Alto, California, but I am originally from Sweden. I am working as CTO on Speedment with Java and database application acceleration. Check it out on www.speedment.com
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
MarcelMA
MarcelMA
4 years ago

I have a question.
How does the construct public Stream from(String[] array) work?
I can’t find anything about this construct.
I see in the test
void fromArray
And still it works when I do a return Stream.of(array) when I declare the array as (“Alpha”, “Bravo”, “Charly”)
I’m curious how the test knows that with fromArray it had to perform Stream from(String[] array)?

Per Minborg
4 years ago
Reply to  MarcelMA

Hi MarcelMA and thanks for your question. I am not sure I really understood what you mean so let me try to answer and let me know if I answered the wrong question. The built-in Java construct Stream.of() takes an array of any type T, so when you, for example, provide a String array the (static) method will return a Stream of type String. The test method Unit1MyCreateTest::fromArray is calling a utility method that takes your unit instance, a reference Stream with the correct answer for the test input and two other Functions that somehow collects your stream and the… Read more »

MarcelMA
MarcelMA
4 years ago
Reply to  Per Minborg

Hi Per, Thanks for your fast and thorough explanation, but that’s not what I had in mind.
I see in the Unit1MyCreateTest.java a void fromString and this seems to test the
public Stream from(Collection collection) from Unit1MyCreate.java.
I have 2 questions about this.
1. How does the test know it has to call the Stream from(Collection collection)?
2. How does this construct Stream from(Collection collection) work?
I can’t find anything about it on the net and from isn’t even a reserved word, if I’m correct.
Still it works!
regards,
Marcel

Per Minborg
4 years ago
Reply to  MarcelMA

Hi again, So the key here is the static method tester() which I wrote to reduce the test code length. This method takes your instance (e.g. your Unit1MyCreate) as the first parameter. Let’s take a closer look at the test fromCollection(): It knows it should call your Unit1MyCreate.from() because of the lambda i -> i.from(texts) reveals this. The tester() has your instance and it now knows that it shall apply the given lambda on your instance and so your instance.from() is invoked within the tester(). I hope this answers both your questions. If not, let me know and I will… Read more »

MarcelMA
MarcelMA
4 years ago
Reply to  Per Minborg

Thanks Per, it’s very interesting.
I think I’m getting my head around it.
I have one more question about the fromCollection():
The last parameter of tester, s -> s.collect(toList()) ,
how does it know it has to get the stream from the instance (if I’m correct)?
Another question:
Can you tell me where and how you learned programming in such a functional style?
Are there any book and/or courses you took?
Thanks for educating me!

Per Minborg
4 years ago
Reply to  MarcelMA

Thanks for the more in-depth questions. The tester() takes as a final argument a lambda that is used 2 times by the tester. The lambda is applied on the reference stream (which holds the expected elements) and then it is applied to the result of the first lambda i -> i.from(texts). In this case, the elements from the two streams are collected to a list. The reason for that is that it is not possible to compare two Streams for equality. But Lists can be compared for equality. I am the maintainer of open-source Speedment Stream ORM which has its… Read more »

MarcelMA
MarcelMA
4 years ago
Reply to  Per Minborg

Thank you Per, I finally understand it completely.
On to part 2!

Back to top button