Streams rundown
Since Java 8, you can use the Stream API to manipulate Collections. They are a fairly semantic way to apply a series of changes to a Collection, producing another different Collection (or object, or variable) as a result. No mutation occurs.
Generally speaking, you turn a Collection into a Stream with .stream()
and pipe a series of operators to produce the desired result.
Each intermediate operator (map
, filter
, sorted
, etc.) takes a Stream as input, while outputting another Stream.
You close the Stream by calling one of the terminal operators: collect
will return a Collection (no surprises here), forEach
will return Void, while reduce
and find
will be covered below.
Map
Runs a given function on each element of the Stream.
Something like inputStream > Map(myFunction(element)) > outputStream
.
For example:
Output: [8, 6, 5, 8]
Filter
Similar to map
, but in this case, the function it receives needs to return a boolean
. This is because filter
will only output the elements of the incoming Stream that return true
after being evaluated with the function it received.
Something like inputStream > Filter(myFilter()) > outputStream
, where myFilter()
returns a boolean
.
Output: [another]
Reduce
Produces a single result from a Stream, by applying a given combining operation to the incoming elements.
There are three possible components in this operation:
- Identity (optional): initial or default value, if the Stream is empty.
- Accumulator: function that takes two parameters:
- Partial result of the operation.
- Next element of the Stream.
- Combiner (optional): function used to combine the partial results when under parallel execution or mismatch between the types of the accumulator.
Something like inputStream > Reduce(myIdentity, myAccumulator, myCombiner) > result
.
Accumulator
Unless the accumulator has some complexity to it, you’ll usually see it as a Lambda:
Output: Java-Streams-Rule
By default, reduce
will return an Optional
of the type it finds in the incoming Stream, hence the if
statement at the end.
You can avoid that part by closing the Stream with an orElse()
.
Identity
Useful for avoiding NullPointerException
s, especially when reducing complex objects.
Output: The product is: 5040
Combiner
Due to some quirks of the JVM when under parallel execution, we’ll need a way to combine the results of each sub-stream in one.
A simple example with the three reduce
components explicitly set might look something like:
Output: 160
The Combiner will also be necessary if different types are managed in the Accumulator. In the example, the Accumulator has an int
as partial result, but a User
as next element:
Find
There are two variants of the find
function in Java:
- findFirst: Deterministically find the first element in the Stream.
- findAny: Return any single element of the Stream, disregarding order.
One always gets the same element (given the same input Stream), while the other does not guarantee it. Bear in mind, that in simple single-threaded examples like these, both are likely to behave in the same way.
Output: Java
By default, findFirst
will return an Optional
of the type it finds in the incoming Stream. Just like we did with our reduce
example, you can avoid handling the Optional by closing the Stream with an orElse()
.