Preface
In the last part, we’ve learned about the Optional type and how to use it correctly. Today, we will learn about Streams, which you use as an 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.
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 Stream.of():
- public void convertObjects() {
- Stream
objectStream = Stream.of("Hello", "World"); - }
Luckily, Oracle has thought through the implementation of Streams in Java 8. Every Class that implements java.util.Collection
- public void convertStuff() {
- String[] array = {"apple", "banana"};
- Set
emptySet = new HashSet<>(); - List
emptyList = new LinkedList<>(); - Stream
arrayStream = Arrays.stream(array); - Stream
setStream = emptySet.stream(); - Stream
listStream = emptyList.stream(); - }
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 use some methods we already heard about when we learned about Optionals also with Streams.
map
This works pretty straight forward. 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.
- public void showMap() {
- Stream.of(1, 2, 3)
- .map(num -> num * num)
- .forEach(System.out::println); // 1 4 9
- }
Like with Optionals, we use flatMap to going e.g from a Stream
- > to Stream
- public void showFlatMapLists() {
- List
numbers1 = Arrays.asList(1, 2, 3); - List
numbers2 = Arrays.asList(4, 5, 6); - Stream.of(numbers1, numbers2) //Stream
- >
- .flatMap(List::stream) //Stream
- .forEach(System.out::println); // 1 2 3 4 5 6
- }
forEach
The 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 e.g. 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.
- public void showForEach() {
- Stream.of(0, 1, 2, 3)
- .forEach(System.out::println); // 0 1 2 3
- }
Filter is a really basic method. It takes a ‘test’ function that takes a value and returns boolean. So it test every object in the Stream. If it passes the test, it will stay in the Stream or otherwise, it will be taken out. This ‘test’ function has the type Function
- public void showFilter() {
- Stream.of(0, 1, 2, 3)
- .filter(num -> num < 2)
- .forEach(System.out::println); // 0 1
- }
- public void negateFilter() {
- Predicate
small = num -> num < 2; - Stream.of(0, 1, 2, 3)
- .filter(small.negate()) // Now every big number passes
- .forEach(System.out::println); // 2 3
- }
- public void filterNull() {
- Stream.of(0, 1, null, 3)
- .filter(Objects::nonNull)
- .map(num -> num * 2) // without filter, you would've got a NullPointerExeception
- .forEach(System.out::println); // 0 2 6
- }
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 times, you convert it into a List or a Set:
- public void showCollect() {
- List
filtered = Stream.of(0, 1, 2, 3) - .filter(num -> num < 2)
- .collect(Collectors.toList());
- }
- public void showJoining() {
- String sentence = Stream.of("Who", "are", "you?")
- .collect(Collectors.joining(" "));
- System.out.println(sentence); // Who are you?
- }
These are methods which you could mostly emulate by using map, filter and collect, but these shortcut method are meant to be used because they declutter your code.
reduce
Reduce is a very cool function. It takes a start parameter of type T and a Function of type BiFunction
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 a lot to make your code more readable if you know how to use it.
- public void showReduceSum() {
- Integer sum = Stream.of(1, 2, 3)
- .reduce(0, Integer::sum);
- System.out.println(sum); // 6
- }
sorted
You can also use Streams to sort your data structure. The class type of the objects in the Stream doesn’t even have to implement Comperable
And this int, like in the compareTo() function, shows us if the first object is “smaller” than the second 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.
- public void showSort() {
- Stream.of(3, 2, 4, 0)
- .sorted((c1, c2) -> c1 - c2)
- .forEach(System.out::println); // 0 2 3 4
- }
There are also special types of Streams which 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,… . 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.
- public void sumWithIntStream() {
- Integer sum = Stream.of(0, 1, 2, 3)
- .mapToInt(num -> num)
- .sum();
- }
Tests can also be used to test your methods. I will use the method anyMatch here, but count, max and 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 if a specific name was stored in the DB.
- @Test
- public void testIfNameIsStored() {
- String testName = "Albert Einstein";
- Datebase names = new Datebase();
- names.drop();
- db.put(testName);
- assertTrue(db.getData()
- .stream()
- .anyMatch(name -> name.equals(testName)));
- }
And after I 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 forEach and sorted. 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.
- public class User {
- private String username;
- private String birthday;
- public User(String username, String birthday) {
- this.username = username;
- this.birthday = birthday;
- }
- public String getUsername() {
- return username;
- }
- public String getBirthday() {
- return birthday;
- }
- }
- public class MainClass {
- public static void main() {
- List
users = new LinkedList<>(); - User birthdayChild = new User("peter", "20.02.1990");
- User otherUser = new User("kid", "23.02.2008");
- User birthdayChild2 = new User("bruce", "20.02.1980");
- users.addAll(Arrays.asList(birthdayChild, otherUser, birthdayChild2));
- greetAllBirthdayChildren(users);
- }
- private static void greetAllBirthdayChildren(List
users) { - // Next Section
- }
- }
Now, we want to greet the birthday children. 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.
- public static void greetAllBirthdayChildren(List
users) { - String today = "20.02"; //Just to make the example easier. In production, you would use LocalDateTime or so.
- users.stream()
- .filter(user -> user.getBirthday().startsWith(today))
- .forEach(user -> sendMessage("Happy birthday, ".concat(user.getUsername()).concat("!"), user));
- }
- private static void sendMessage(String message, User receiver) {
- //...
- }
Parallelism
Streams can also be executed 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.
Conclusion
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.
Supplement
* Part 0 - Motivation
* Part 1 - Functions as Objects
* Part 2 - Optionals
* Part 3 - Streams
* Part 4 - Splitter
沒有留言:
張貼留言