Java 8 Streams, a practical introduction | Jeronemo | Skillshare

Playback Speed


1.0x


  • 0.5x
  • 0.75x
  • 1x (Normal)
  • 1.25x
  • 1.5x
  • 1.75x
  • 2x

Java 8 Streams, a practical introduction

teacher avatar Jeronemo

Watch this class and thousands more

Get unlimited access to every class
Taught by industry leaders & working professionals
Topics include illustration, design, photography, and more

Watch this class and thousands more

Get unlimited access to every class
Taught by industry leaders & working professionals
Topics include illustration, design, photography, and more

Lessons in This Class

    • 1.

      Introduction

      0:41

    • 2.

      Stream Usage and Use Case

      3:28

    • 3.

      Functional Interfaces & Lambda

      10:48

    • 4.

      Stream Operations

      1:52

    • 5.

      Intermediate Operation: filter

      1:19

    • 6.

      Terminal Operation: forEach

      1:31

    • 7.

      Intermediate Operation: map

      1:34

    • 8.

      Intermediate Operation: flatMap

      2:36

    • 9.

      Intermediate Operations: sequential & parallel

      2:48

    • 10.

      Intermediate Operation: peek

      1:33

    • 11.

      Terminal Operation: collect

      4:06

    • 12.

      Terminal Operation: reduce

      5:11

    • 13.

      Summary & Afterword

      1:31

  • --
  • Beginner level
  • Intermediate level
  • Advanced level
  • All levels

Community Generated

The level is determined by a majority opinion of students who have reviewed this class. The teacher's recommendation is shown until at least 5 student responses are collected.

34

Students

--

Project

About This Class

Streams were introduced in Java 8, but are just as relevant in Java 11, Java 17 and all versions to come. They're a key aspect in becoming a better Java developer.

This class will teach you everything you need to know about Java Streams to start using them in your own projects. Its goal is to give you insight into the possibilities of Streams, as well as letting you recognize situations in which Streams can be useful. It mainly focusses on those having little to no experience with Streams & lambdas, but can also be used as a refresher course on the most commonly used operations and their usage.

What you'll learn in this course:

  • How Anonymous interclasses, Functional Interfaces & lambdas work and are created.
  • How Streams work in general and why you should use them.
  • How the most commonly used operations on a Stream work.

Streams are a step into functional programming. Mastering Streams makes your code readable like a story, which in turn makes it all the more maintainable by you and others. There's exercises at the end to put it all into practise.

Who this course is for:

Any Java developer interested in Streams, or Functional programming in general. While the title mentions Java 8 (which was its introduction version), it's a great skill to have no matter what version of Java you work on. You are expected to have a basic understanding of using Java, like compiling and running code, as well as extending classes and implementing interfaces.

Materials needed for this course:

No materials are needed to follow along with this course. If you wish to do the exercises at the end however, you'll need to have JDK 8 or higher installed. A way to edit and run the .java files is also needed. This could be as simple as notepad, but I recommend using an IDE like IntelliJ or Eclipse.
That's it!

Meet Your Teacher

Teacher Profile Image

Jeronemo

Teacher

Hey there, welcome to my profile! I have the classical Dutch name Jeroen, but you may call me Jeronemo.

I've been a backend developer for the good part of a decade, dabbing mostly in microservice architecture using REST, Java & Spring Boot, but having worked with loads of other stuff like ADO, Docker, ELK stack, Eureka, Flyway, Gitlab, GWT, Hazelcast, Hibernate, Hoverfly, IntelliJ, Jackson, Jenkins, JIRA, JSON, Junit, Kubernetes, RabbitMQ, Maven, Mockito, Nexus, SOAP, Sonar, SQL, WDSL, XSD & Zuul.

See full profile

Level: All Levels

Class Ratings

Expectations Met?
    Exceeded!
  • 0%
  • Yes
  • 0%
  • Somewhat
  • 0%
  • Not really
  • 0%

Why Join Skillshare?

Take award-winning Skillshare Original Classes

Each class has short lessons, hands-on projects

Your membership supports Skillshare teachers

Learn From Anywhere

Take classes on the go with the Skillshare app. Stream or download to watch on the plane, the subway, or wherever you learn best.

Transcripts

1. Introduction: Hey everyone, welcome to the class Java eight streams, a practical introduction. My name is journeyman, and in this class I will be telling you all about streams in their usage. This class is aimed to dose, having little or no experience with streams is gauze to give you insight into the possibility of streams. You recognize situations in which streams can be useful. We'll dive deeper into streams usage and wide. You want to use them into functional interfaces and lambdas. I will explain commonly used operations for those craving or more hands-on approach. This course also offers exercises to put the knowledge learned into practice. Let's get started. 2. Stream Usage and Use Case: Streams usage and use case. Water streams use for simply said, a stream can be used to process a collection of items. In a later chapter, we'll dive deeper into some of the specific ways to process a collection. But for now, just think of processing as filtering, changing, and enriching the data for collection. Do note, however, productions used as input for streams aren't actually changed by the streams. Instead, executing a stream gives us separate results disconnected from the original collection. These operations processing steps are written in a functionally descriptive way. Meaning a stream reads like a story when done correctly, which is what makes them so incredibly useful. To showcase this. Here's an example of a conventional way of filtering, collecting, sorting, and printing some data. I use the same logic written in streams. While I'm sure you'll be able to understand both sides eventually, I think we can all agree that using streams makes it much, much easier to understand what the code does. So there you have it. Streams are used to process a collection of items in a very reasonable way. How does streams work? As just said, streams work on a collection of items. You might have realized that the word collection matches an interface. In Java. Interface found in the Java util package is our linked to work in with streams. Most of the time, you commonly use a list or set to start a stream which extend the collection interface. There are other ways of creating a stream. We won't go into them in this class. You can find them in the resources. With the arrival of Java eight, a default methods called stream has been added to the collection interface. For those unfamiliar with default methods. Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older version of the interfaces. This default method stream returns an instance of the stream costs the central API class that allows us to work with them. After creating the stream, you can now chain operations together and execute the stream. More on this later. Let's summarize what we've learned so far. Streams are used to process a collection of items in a very readable way. Streams don't change this source collection, but instead create a separate result disconnected from the original collection. The stream interface is the central API cost of working with streams. Anything extending or implementing the collection interface has a default stream method to interact with sets central API class. 3. Functional Interfaces & Lambda: Functional interfaces and lambdas. Before we can start talking about stream operations, we'll need to talk about the prerequisites of using strips. The lambdas. Lambdas are basically anonymous inner classes, functional interfaces of which the Java compiler implicit denotes the needed information. Let's start from the beginning. Anonymous inner classes. When implementing an interface, you're going to implement all its methods. Here's an example where you create implemented class and instantiate it. Here we have an interface called my interface with the print. Something important method is implemented by my interface. But what if I need a slightly different implementation of the prints, something important method. In this scenario, I would have to create another class that implements my interface. Again. Imagine if you have ten different variance, it will become cluttered real soon. That's a cue for anonymous inner classes. Anonymous inner class is an extension of a class. One implementation of an interface without a name, hence anonymous. We'll focus on the interface part S. That's the important one in the context of streams. Since they have no name, we can't create an instance of the anonymous inner class on its own. Instead, you need to declare and instantiate the anonymous inner class in a single expression. This expression looks as follows, which translates to the following. When creating the anonymous inner class version of my interface info. As you can see, we've created an instance of my interface without needing a class implementing the interface. And if I wish to add a new implementation of my interface, I can just do so. There we have it. Anonymous inner classes can be used to implement an interface without needing to define a rigid class. You can just create them on the fly. Functional interfaces. Functional interface is just an interface except it only contains a single method that must be implemented. Any interface that meets that criteria is a functional interface. Just like our my interface example just now. While not needed, you can add the functional interface annotation to show your intent. As an added bonus, the compiler will throw an exception compile-time when it's not actually a functional interface when using this annotation. Let's go through the most common functional interfaces used when working with streams. Are first one is the supplier with admitted gets. It doesn't expect any arguments, but does return something. The supplier supplies you with something, usually a new, empty, clean object. You could see it as a factory. The next one is predicate. With admitted. It expects one argument, evaluates it, and returns a Boolean. As the name implies, is basically a test. Does the argument t meet the given criteria yes or no? Consumer with its methods accepts, accepts one argument, does something with it, but doesn't return anything, literally consuming the arguments in the process. The bike consumer does the same as consumer, except it expects two arguments. Function with its methods apply. Expects an argument, t does something with it. And returns in arguments are basically mapping or enriching the argument in the process, the argument r and t can be the same class. The by function does the same as function. Excepted expects two arguments, T and U does something with them. I'm returns are just like the function. Arguments can be the same class if need be. And on that note, we arrive at the last common functional interface. Binary operator. Binary operator is an extension of by function where T, U, and R are the exact same class. Make sure you have a decent understanding of functional interfaces in general and off these common ones before continuing. As it will help you understand lambdas and stream operations more easily. Lambda expressions. Most more lumped us are anonymous inner classes of functional interfaces of which the Java compiler implicitly knows the needed information. We now know what a anonymous inner class is and how we still need to implement all methods of an interface. We notice functional interfaces are regular interfaces, except they only have a single method that must be implemented. Since a functional interface only has a single method, the compiler is able to understand a lot of information based on context. You, as a developer can leave out that information. So lets the compiler know you're leaving things out. The Lambda syntax issues. We'll start with irregular anonymous inner class and convert it to a full-blown lambda expression piece-by-piece. Or use the functional interface predicate to test if a given integer is higher than one. Looking after the equals sign, we noticed the name of the interface we're implementing. The body of the anonymous inner class, the method signature, and the body of the methods we're implementing. Look closer at the interface name and the method signature. Do you see how the compiler could infer this knowledge? Notice how the interface names are already on the left side of the equal statements. Notice how since predicate is a functional interface, there's only a single method to be implemented. So we already know which methods we are implementing. Both of these kinds of information can be inferred by the compiler. Our first step is to write this using the lumped as syntax, the arrow. With it. We'll get rid of the interface name, method name. This already looks a lot cleaner right? Here we see the input of the methods, the integer on the left side of the arrow, and the body of the method on the right side. But there's still some information we can leave out in this case. Since there's a single method and the compiler knows the signature of it, the compiler can infer the input type from the context. The brackets around the inputs have disappeared as well. This is only possible because it's a single parameter. In case you have two or more, the brackets are mandatory. There's one more thing we can now infer. It has to do with the body and the return statement. In the case of having a single line of code in the body of the method, the compiler can infer that that line of code is in fact the return statement of the body. With this final step, we've reached the full-blown lambda expression. You will see all the time when working with strings. Actually, there's a way to write the lambda even shorter. In some cases. This is called method reference. But I suggest to look it up yourself when you have a good grasp of long does in general. One final important thing to mention about lumped us is that local variables used in them should be final or effectively final. Effectively final means a variable that doesn't have the final keyword, but its value doesn't change after its first assigned. The reason for this is that lumped us are basically a piece of code you can run within other methods. Like how you can pass our predicate to a filter operation within a string. For this reason, Java needs to create a copy of the local variable to be used within the lungs. To avoid concurrency problems. The alpha has decided to restrict local variables altogether. Let's summarize what we've learned so far. Anonymous inner classes are inner classes that implements an interface without having a name, does need to be declared and instantiated that once. Functional interfaces are regularly interfaces except the only contain a single method that must be implemented. The functional interface annotation can be used to enforce this rule is not needed. Lumped us are anonymous inner classes of functional interfaces of which the Java compiler can infer missing information based on the context. We now know how to create a lambda expression and of irregular anonymous inner class. And lastly, local variables used in lumped us must be final or effectively fine. 4. Stream Operations: In the next chapters, I'll explain commonly used operations on strings. But first, we should talk about the two types of possible operations, intermediate and terminal operations. A stream only gets fully executed when a terminal operation is used. Therefore, a stream always ends in a terminal operation and can have only one of them in a stream. Intermediate operations are all operations before we hit the final terminal operation. Intermediate operations return an instance of stream, making it possible to chain them all together. Since all intermediate operations returns stream, you can store a string without terminal operations in a variable. Thanks today, so you can actually add steps to your stream based on external influences, like a different filter or a different way of enriching the data. Pay attention though, even though you can store intermediate operations in variables, it's not possible to execute the same stream twice with a terminal operation. The compiler doesn't recognize this, but you'll get an exception like this one during runtime. Once more. Intermediate operations return a stream and can therefore be linked and stored in variables. Intermediate operations alone don't execute a stream. A terminal operation executes the full stream and returns a value other than a stream. A stream can only be executed once with a terminal operation. Attempting to do it twice results in an exception during runtime. With this knowledge, we'll go on to the commonly used operations. 5. Intermediate Operation: filter: Let's start with the basics. Filter. Filter is an intermediate operation that lets you filter out objects from strings that do not pass the given test. You could see the intermediate operation filter as the stream equivalent of an if statement. It's input is a predicate which as we learned, receives an object, performs a test on it, returns true or false depending on if it passed the test or not. Here's an example of its use. And don't worry about the other operations. We'll get to those soon. In this example, we use filter to continue the stream only with objects whose list of phone numbers is empty. Before returning to remaining customers as a list, filtering can be done using any effectively final value. Here, we're filtering based on our local variable, e.g. we can also chain them together using multiple filters in a row. The filter operation is a prime example of how streams make your code more readable. Especially if you stored a long time in a value like so. You can now see what we're filtering on at a glance. 6. Terminal Operation: forEach: Our first terminal operation for each. The forEach is a very straightforward terminal operation. It's used to perform a certain action on all elements in the stream. Assuming the elements in the process, its inputs, is a consumer. So it takes an object, does something with it, but doesn't return any value. Afterwards. We've seen its implementation in the previously discussed operation, but here it is once more. In this example, we print the first name of every customer industry. Keep in mind that for each only operate on the elements once they've gone through the rest of the stream. So if we put a filter before the terminal operation, like so, then only customers with less than three devices get their first name, print it. Finally, did she know that a normal methods can be used in a lumped as well? Take this method e.g. doesn't that look like a consumer implementation to you? It accepts a single object, does something with it, but doesn't return any value afterwards. And indeed, we can use this method in our forEach, making it more readable in the process. And that's it. For each simply performs a specified action for each element of the stream that makes it through the rest of the stream. 7. Intermediate Operation: map: Another basic intermediate operation, map. Map is an intermediate operation that's commonly used to map from one type of object to another one. But it is typically also used to perform business logic. Its inputs is a function which as we now know, it takes an object of class a, does something with it, and returns an object of class b where clause a and B can be the same class. Here's an example where we map from class a to B. Here we have a customer object as inputs. We map that to a string comprised of his or her first and last name before printing it out. In this case, we're mapping from class a to class B, from customer to string. In this next example, we have a customer object as input at work mobile to their devices. Then return the same customer object, effectively enriching your object itself. In this case, we're mapping from customer to customer and reaching the object along the way. Do note that since the Lambda body contains two statements, I had to add the brackets and return keyword back in. Finally, just like with any intermediate operation, it is possible to have multiple math operations within the same stream. 8. Intermediate Operation: flatMap: Next up, flatMap. As the name suggests, flatMap is similar to map. It is an intermediate operation that is commonly used to map from one type of object to another. With the difference being that flatMap works on one-to-many relations. It's input is also a function, but a restriction is imposed on the return object. A stream should be returned. Here we see the method signature of map and flatMap side-by-side to show you the difference. While both expect the function, flatMap expects the r value to be of type string. Let's have a look at what it does and how it differs from that. In our examples, we've used the customer object, which has a list of devices. What do you think would happen if we were to use the map operation to convert each customer to their devices. As you can see in the output, mapping from a customer to a list of devices, results in a stream of lists of devices. When printed out, these lists are printed one by one. This might be something you need for your functional use case. But more often than not, you're interested in all separate devices rather than separate lists of devices. This is where flatMap comes in. As said before. Flatmap imposes a restriction on the return object of the function. You can see that the lambda has slightly changed for flatMap and now returns the device is a stream. Instead of as list. Notice in the outputs that the devices are now printed one-by-one instead of list by list. The key here is that flatMap expect streams as results which are then flattened into a single stream again. Once more together. You can now clearly see the difference between the map and flatMap operation. To summarize, maps should be used when converting objects with a one-to-one relation. While flatMap should be used when converting objects with a one-to-many relation. When using flatMap, the return value is a stream of objects. 9. Intermediate Operations: sequential & parallel: Two operations of the same coin, sequential and parallel. Sequential and parallel are both intermediate operations that make the full stream sequential or parallel respectively. Since they are intermediate operations, they can be added to the stream together even multiple times. It doesn't matter where in the stream the operation is placed. The entire stream is either sequential or parallel. Not a bit of both. Keep in mind that the last sequential or parallel operation mentioned industry, the one that is actually used. That means that this stream is exactly the same as this stream. A stream is sequential by default, meaning the elements are executed in the order they appear in the US collection. That also means that these streams have the same output. As you can see. This results in printing out one to ten in order with or without the sequential operation. When we change that to parallel, however, as you can see, we get a different result every time we run the Stream. Stream is executed multi-threaded, meaning that the work done is split between different executes. The so-called threats. Keep in mind that making a stream run parallel doesn't automatically mean the stream can be run thread-safe. You, as the developer, have to ensure that your code works exactly the same, 1236 or whatever threats. As it doesn't want. Another thing to keep in mind about running things in parallel is that it doesn't necessarily mean the work is done faster. Making something multi-threaded means you need to split the work in parts, create an executed for every part. Delegated work, emerge the different results into one. This results in some overhead when running the code or stream, in our case, which will only be beneficial if you have enough elements in your stream or if your stream is very compute heavy. To summarize, the sequential and parallel operations allow you to quickly make the entire stream sequential or parallel. Running a stream parallel, you, as the developer, have to ensure that the code is thread safe. 10. Intermediate Operation: peek: Let's go with a simple one before we move to the complex ones. Pq. Pq is an intermediate operation that literally lets you take a peek behind the scenes when executing a string. Its main use is debugging and logging its inputs. As a consumer, meaning it receives one value and does something with it without there being a return value. While it's possible to modify data within the peak operation, you take a risk in doing so. Depending on your Java version and whether or not your terminal operation has a need to process the objects in the string. The code inside peek may or may not be run. Be very careful when using peak for anything other than debugging and logging. Here's an example of its use. In this example, we use peak to print a name of a device before we continue mapping it to its order details, collecting those potential further processing. One result of this stream is a list of order details. Thanks to the peak operation, we can gain some logging with a device objects use for the results. And I saw peak is simply a way to get some logging during a stream. You can use it to modify data, but it's risky, so I would advise against it when you're not yet familiar with streams. 11. Terminal Operation: collect: Our second terminal operation, collect. Collect is a terminal operation that collects all the items of a stream into something. It's commonly used to put the resulting items of a stream into a list. There's two implementations for this one. The first one expects an implementation of the collector interface. This is not a functional interface however, and can therefore not rewritten as a lambda expression. Luckily for us, yeah, Lava has been so kind as to provide the collectors clusters containing all sorts of helpful methods. Many are beyond the scope of this workshop. But there's one you'll frequently use collectors dot two lists. As you might expect. This returns an implementation of the collector interface that collects all elements of the string into a list and returns it. I personally use this 190, 9% of the times I need to collect something and expect you to experience the same. But in order to understand what it does a little better, let's go over the second implementation. This one expects a supplier and two by consumers. The Java doc gives us a nice overview of what it does. The supplier supplies us with that, which we expect as a result from the collect operation. Than the first bike consumer is used to consume an element of the stream into this results. This continues until all elements from the stream or consumed into the final results. This doesn't show us what the second bi consumer to combine it does, however, that's because the combiner is only needed when the stream is ran parallel. In that case, you might have two or more threads starting out with the supplier. After all threats are done, the results of those threads must be combined into a single results, which is exactly what a combiner is used for. Let's put it into practice. Using a supplier and two by consumers. I will replicate collecting the customer elements into a stream. We'll start with the supplier. When calls will get the empty list where we'll put all elements of the stream into. Next, we need a bike consumer that accumulates all elements of the string into the supply list. This makes it clear that our result is indeed that list and that every customer elements from the stream is accumulated into set list. In case of a sequential stream, that will be the end of it. But to allow for parallel processing, will need a final by consumer that combines two results into one. Using this by consumer, we add the elements of the second result into the first one. This is repeated by the stream until all threats results are merged back into one. Here they are filled into the collect operation. And with that, you know how to custom make your own collects. To reiterate, there are two implementations of the collect cooperation. The first one expects a collector of which Java has prepared some options for you using the collectors clause. The second one expects a supplier and to bind consumers, making it a lot more customizable. You'll use the prepared collect this class most of the time. But at least now you know how it works. 12. Terminal Operation: reduce: The final operator will discuss reduce. Reduce is a terminal operation that can be used to reduce all elements of a stream into a single object. It's a bit like to collect operation we discussed. But whereas collect applies all elements of the stream to immutable container like a list or set. Reduce doesn't collect but applies all elements to a single object. The identity. Reduce, takes his identity, accumulate or elements into the identity, then combines multiple results together into one in case it's run parallel. Reduce is the hardest operation to grasp. So don't feel bad if you don't get it all at once. Just take your time to follow this section and try the exercises at the end of the class. If you have any questions left, feel free to ask them using the platform. Alright, let's dive in. Deep reduce expects a start value called the identity function that's used as accumulating and a binary operator used to combine results from separate threads. The alpha dog gives us a nice overview of what it does. This identity gives us the start of the results. It should be clean, empty slate, such that adding any value from the stream to this identity has that result and altered as product. Then for every element from the stream, it is accumulated into the resulting value using the by function. Just as with to collect, this doesn't show us what the binary operator, the combiner does. The same applies here. The combiner is only used to combine the results of multiple threads together in case the stream is ran parallel. Let's put it into practice. For our example, we'll, we'll choose all customers into an integer representing the total amount of devices owned by those customers together. Our identity should be an integer. As a result, we want out of this stream. As far as value. Remember how adding any result to the identity should have to result in altered as product. In our case, we'll have to add the size of all list of devices together to come to our final results. Which value of the identity means that identity plus size of device list equals two. The device this, That's right, zero. So our identity will be like this. Next, we need a function that produces an element of the stream into this identity. The first generics used in the by function is an integer which represents the current value of the results. The subtotal, if you will. The second generics use is the element of the stream, which will reduce into the subtotal. The third generics use is the type of the result of the operation an integer. This third generics must be the same as the first one, which is logical as the first generic term represents the subtotal. In case of a sequential stream, that would be the end of it. But to allow for parallel processing, will need to binary operator to combine two results into one. Using this binary operator, we combine the results of two threads together. This is repeated by the stream until all threads results are merged back into one. Here they are filled into the reduce operation. To reiterate, we start with a clean, empty slate as identity zero. In our case. We then add the size of the list of devices of every elements from the stream to this identity. If the stream is ran parallel, we use to combine them to add two subtotals into one until we have a single result left. In our example, we reduced our complex objects into a single integer. But reduced can also be used to reduce all elements of a stream into a single complex object. E.g. we could reduce our customers into a single customer containing our info. But I'll leave that one as part of the exercises you can do on your own. 13. Summary & Afterword: We've come to the end of this class. Let's summarize to see what we've learned on our journey into the Java eight streams API. We started out learning what streams are used for and how they work. We then broke down how you can create a lambda expression out of an anonymous inner class thanks to functional interfaces. We briefly discussed, but functional interfaces are, and which ones you commonly used when working with streams. Finally, we went over commonly used intermediate and terminal operations. Thank you for watching this class. I hope you've learned enough about streams to recognize situations in which you can and are able to use them. If you wish to help me improve this class and tell us whether or not you found it useful. I would be grateful if you left a review. As promised. I've prepared some exercises for you to go through to see if you can apply the knowledge learned during this course. Every exercise has a request, an expected outcome for you to check if you did it right. If there's anything unclear about this class already provided exercises, feel free to send me a message with your questions. I will help you out to the best of my ability. Once more. Thank you for watching and until next time.