In the last part, we learned about the Optional type and how to use it correctly.
Today, we will learn about
Streams, which you use as a functional alternative of working with Collections. Some method were already seen when we used Optionals, so be sure to check out the part about Optionals.
Where Do We Use Streams?
You might ask what’s wrong with the current way to store multiple objects? Why shouldn’t you use Lists, Sets, and so on anymore?
I want to point out: Nothing is wrong with them. But when you want to work functional (what you hopefully want after the last parts of this blog), you should consider using them. The standard workflow is to convert your data structure into a Stream. Then, you want to work on them in a functional manner and, in the end, you transform them back into the data structure of your choice.
And that’s the reason we will learn to transform the most common data structures into streams.
Why Do We Use Streams?
Streams are a wonderful new way to work with data collections. They were introduced in Java 8. One of the many reasons you should use them is the Cascade pattern that Streams use. This basically means that almost every Stream method returns the Stream again, so you can continue to work with it. In the next sections, you will see how this works and that it makes the code nicer.
Streams are also immutable. So every time you manipulate it, you create a new Stream.
Another nice thing about them is that they respect the properties of fP. If you convert a Data Structure into a Stream and work on it, the original data structure won’t be changed. So no side effects here!
How to Convert Data Structures Into Streams
Convert Multiple Objects Into a Stream
If you want to make a Stream out of some objects, you can use the method
Converting Collections (Lists, Sets, etc.) and Arrays
Luckily, Oracle has thought through the implementation of Streams in Java 8. Every Class that implements java.util.Collection<T> has a new method called
stream() , which converts the collection into a Stream. Also, Arrays can be converted easily with
Arrays.stream(array). It’s as easy as it gets.
However, normally, you won’t store a Stream in an object. You just work with them and convert them back into your desired data structure.
Working With Streams
As I already said, Streams are the way to work with data structures functional. And now we will learn about the most common methods to use.
As a side note: In the next sections, I will use
T as the type of the objects in the Stream.
Methods We Already Know
You can also use some methods we already heard about when we learned about Optionals with Streams.
This is pretty straightforward. Instead of manipulating one item, which might be in the Optional, we manipulate all items in a stream. So if you have a function that squares a number, you can use Map to use this function over multiple numbers without writing a new function for lists.
Like with Optionals, we use flatMap to go, for example, from a Stream<List<Integer>> to Stream<Integer>. If you want to know more about them, look into part two.
Here, we want to concat multiple Lists into one big List.
And in these examples, we already saw another Stream method,
forEach(), which I will describe now.
Common Stream Methods
forEach method is like the
ifPresent method from Optionals, so you use it when you have side effects. As already shown, you use it to, for example, print all objects in a stream. forEach is one of the few Stream methods that doesn’t return the Stream, so you use it as the last method of a Stream and only once.
You should be careful when using forEach because it causes side effects, which we don’t want to have. So think twice if you could replace it with another method without side effects.
Filter is a really basic method. It takes a ‘test’ function that takes a value and returns boolean. So it tests every object in the Stream. If it passes the test, it will stay in the Stream. Otherwise, it will be taken out.
This ‘test’ function has the type
Function<T, Boolean>. In the JavaDoc, you will see that the test function really is of the type
Predicate<T>. But this is just a short form for every function that takes one parameter and returns a boolean.
Functions that can make your life way easier when creating ‘test’ functions are
The first one basically negates the test. Every object that doesn’t pass the original test will pass the negated test and vice versa.
The second one can be used as a method reference to get rid of every null object in the Stream. This will help you to prevent
NullPointerExeptions when, for example, mapping functions.
As I already said, you want to transform your stream back into another data structure. And that is what you use Collect for. And most of the time, you convert it into a List or a Set.
But you can use collect for much more. For example, you can join Strings. Therefore, you don’t have the nasty delimiter at the end of the string.
These are methods that you could mostly emulate by using map, filter, and collect, but these shortcut methods are meant to be used because they declutter your code.
Reduce is a very cool function.
It takes a start parameter of type T and a Function of type
BiFunction<T, T, T>. If you have a BiFunction where all types are the same,
BinaryOperator<T> is a shortcut for that.
It basically stores all objects in the stream to one object. You can concat all Strings into one String, sum all numbers, and so on. There, your start parameters would be the empty String or zero. This function helps make your code more readable if you know how to use it.
Now I will give a little bit more information on how reduce works. Here, it sums:
The first number with...
...the sum of the second and...
...the sum of the third and start parameter.
As you can see, this produces a long chain of functions. In the end, we have sum(1, sum(2, sum(3, 0))). And this will be computed from right to left, or from the inside out. This is also the reason we need a start parameter — because otherwise, the chain wouldn't have a point where it could end.
You can also use Streams to sort your data structures.
The class type of the objects in the Stream doesn’t even have to implement
Comperable<T>, because you can write your own
Comperator<T>. This is basically a
BiFunction<T, T, int>, but Comperator is a shortcut for all BiFunctions that take two arguments of the same type and return an int.
And this int, like in the
compareTo() function, shows us if the first object is “smaller” than the first one (int < 0), is as big as the second (int == 0), or is bigger than the second one (int > 0). The sorted function of the Stream will interpret these ints and will sort the elements with the help of them.
Other Kinds of Streams
There are also special types of Streams that only contains numbers. With these new Streams, you also have a new set of methods.
Here, I will introduce IntStream and Sum, but there are also LongStream, DoubleStream, etc. You can read more about them in the JavaDoc.
To convert a normal Stream into an IntStream, you have to use
mapToInt. It does exactly the same as the normal map, but you get a IntStream back. Of course, you have to give the mapToInt function another function which will return an int.
In the example, I will show you how to sum numbers without reduce, but with an IntStream.
Use Streams for Tests
Tests can also be used to test your methods. I will use the method
anyMatch here, but
maxand so on can help you too.
If something goes wrong in your program, use
peek to log data. It’s like forEach, but also returns the stream. As always, look into the JavaDoc to find other cool methods.
anyMatch is a little bit like filter, but it tells you if anything passes the filter. You can use this in
assertTrue() tests, where you just want to look if at least one object has a specific property.
In the next example, I will test whether a specific name was stored in the DB.
Shortcuts of Shortcuts
And now that I've showed you some shortcut methods, I want to tell you that there are many more. There are even shortcuts of shortcuts! One example would be
forEachOrdered, which combines
If you are interested in other helpful methods, look into the JavaDoc. I’m sure you are prepared to understand it and find the methods that you need. Always remember: If your code looks ugly, there’s a better method to use.
A Bigger Example
In this example, we want to send a message to every user whose birthday’s today.
The User Class
A user is defined by their username and birthday. The birthdays will be in the format “day.month.year”, but we won’t do much checking for this in today’s example.
To store all users, we will use a List here. In a real program, you might want to switch to a DB.
Now, we want to greet the birthday boys and girls.
So first off, we have to filter out all Users whose birthday is today. After this, we have to message them. So let’s do this.
I won’t implement
sendMessage(String message, User receiver) here, but it just sends a message to a given user.
And now we can send greetings to the users. How nice and easy was that?!
Streams can also be executed in parallel. By default, every Stream isn’t parallel, but you can use
.parallelStream() with Streams to make them parallel.
Although it can be cool to use this to make your program faster, you should be careful with it. As shown on this site, things like sorting can be messed up by parallelism.
So be prepared to run into nasty bugs with parallel Streams, although it can make your program significantly faster.
That’s it for today!
We have learned a lot about Streams in Java. We learned how to convert a data structure into a Stream, how to work with a Stream, and how to convert your Stream back into a data structure.
I have introduced the most common methods and when you should use them.
In the end, we tested our knowledge with a bigger example where we greeted all birthday children.
In the next part of this series, we will have a big example where we are going to use Streams. But I won’t tell you the example yet, so hopefully, you'll be surprised.
Are there any Stream methods that you miss in the post? Please let me know in the comments. I’d love to hear your feedback, too!