DEV Community

Cover image for Functional handling of Collections in Java using Streams

Functional handling of Collections in Java using Streams

Jannik Wempe
Serverless and Frontend Enthusiast
・4 min read

Java Streams were introduced in Java 8. They provide a declarative (or more precise: functional) API to work with collections. The has the following description in the java docs.

Classes to support functional-style operations on streams of elements, such as map-reduce transformations on collections.

There is more about streams besides a functional way to handle collections. It is also about lazy evaluation, parallelism and streams not using collections (e.g. infinite streams, IntStream, ...). But covering all these concepts exceed the scope of this article and therefore aren't described here (Maybe in an additional article?).

Comparison between functional and imperative paradigm

The following repl shows a comparison between a functional and declarative way of summing up som numbers. In my opinion the declarative way using streams isn't only more concise, but also more readable. Therefore, I prefer the functional style.

Creating streams

The interface Collection (see documentation) contains the method default Stream<E> stream() {...};, which returns a Stream instance. Commonly used interfaces like List, Stack and Queue implement the Collection interface and thus you can create a Stream by calling the stream() method on instances of classes implementing these interfaces (e.g. HashSet).

Bonus: infinite streams

There are also so called infinite streams, which are lazily evaluated (which means they are not created ahead of time; they have to, because otherwise they would require an infinite amount of memory). Make sure to limit the stream in order to prevent the program to potentially run endlessly.

Using streams / stream pipeline

There are two different kinds of operations you can do on a stream: intermediate and terminal operations. Chaining these operations creates a stream pipeline. Some operations are shown by examples below. You can read details about intermediate and terminal operations in the docs.

Intermediate operations

Intermediate operations return another Stream, so you can chain another operator to it. The following subsections show some examples. These are commonly used, but there are more (like flatMap or peek).

Stream<T> filter(Predicate<? super T> predicate) (docs)

Returns a stream consisting of the elements of this stream that match the given predicate.

Example: Getting only books with more than 400 pages.

Stream<T> sorted(Comparator<? super T> comparator) (docs)

Returns a stream consisting of the elements of this stream, sorted according to the provided Comparator.

What? Sort all books by their title.

Hint: there is also the Stream<T> sorted() method, which can be used if T implements the Comparable interfaces compareTo(T o) method.

<R> Stream<R> map(Function<? super T,? extends R> mapper) (docs)

Returns a stream consisting of the results of applying the given function to the elements of this stream.

What? Getting only the title of all books.

Combining filter, sorted and map

What? Get only books with more than 400 pages, sort them by title and map them to only their title.

By reading the What? descriptions, you realize that this is all about declarative programming.

Terminal operations

Terminal operations terminate the stream pipeline. They either produce a side-effect (like printing) or a result (like reducing). After a terminal operation, the stream is consumed.

void forEach(Consumer<? super T> action) (docs)

Performs an action for each element of this stream.

forEach produces a side-effect (it returns void, so it has to produce a side-effect, otherwise it would just do nothing). forEach is used in all previous examples to print the result.

T reduce(T identity, BinaryOperator<T> accumulator) (docs)

Performs a reduction on the elements of this stream, using the provided identity value and an associative accumulation function, and returns the reduced value.

What? Get the sum of all pages of all the books.

Hint: There are also other overloads of the reduce method.

<R,A> R collect(Collector<? super T,A,R> collector) (docs)

Performs a mutable reduction operation on the elements of this stream using a Collector.

What? Get a Set containing the titles of all books.

You can do a lot more with Collectors. You could also group the result (imagine grouping books by author or genre).

Hint: There are also other overloads of the collect method.

Feedback welcome

This is my very first blog post. I'd really appreciate your feedback. What did you (not) like? And why? Please let me know, so I can improve the content.

I also try to create valuable content on Twitter. Just have a look.

Discussion (2)

iquardt profile image
Iven Marquardt • Edited

I just want to clarify what the term Stream means in FP. We already have stream-like behavior with lazy linked Lists. Lists differ from Streams in two properties though:

  • they are a sum type (tagged union) with Cons/Nil as cases, whereas Stream doesn't need a Nil case
  • the tail of the List isn't wrapped in a monad so pulling out new elements cannot depend on an effect (this again is possible with the next step in Streams)

Streams are more expressive than Lists and you can apply fusion on it (avoiding intermediate structures) but it also loses some features like sharing, for instance (you can share a list).

jannikwempe profile image
Jannik Wempe Author

Hi Iven,
thanks for the clarification, awesome!

Of course this article is more focused on the provided API than the underlying concept of streams. Therefore your comment is a nice addition.