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.