Hello again! This post continues the series of posts about functional Java programming. You can read also my previous parts about Vavr Option vs. Java Optional and handling exceptions with vanila Java vs. Vavr Try. Now we will talk about streams. Both core Java and Vavr supply streams that are very handy tool and together with aforesaid Optional/Option and Try enables functional style of your applications. As usual we will start from vanilla Java – first describe what is a stream and how to build pipelines. After it we will dive deeper into Vavr Stream and check how it is different from what Java gives us out of the box.
Table of Contents
- What is stream in Java
- Create Java streams
- Use Stream.Builder
- Assemble stream pipeline
- What is Vavr stream
- Create Vavr streams
- Vavr stream operations
What is stream in Java?
Streams were introduced in Java 8 and were updated in next releases. Documentation describes a stream as a sequence of elements supporting sequential and parallel aggregate operations. Please don’t confuse the word “stream”: even before 8th version, Java had InputStream
and OutputStream
, but these concepts have nothing in common with the hero of this post. Java Stream, that was introduced in Java 8 is an implementation of monad pattern – a concept that was brought from functional languages. There, monads stand for computations that are defined as a sequence of steps.
Let have a look on a simple case that was written in a traditional manner:
List<String> names = Arrays.asList("Anna", "Bob", "Carolina", "Denis", "Anna", "Jack", "Marketa", "Simon", "Anna"); for (String name: names){ if (name.equalsIgnoreCase("Anna")){ System.out.println(name); } }
What we do here is that we find all Annas in our list and just print them. This is a simple operation, but, nevertheless, requires us to write a lot of code for such ridiculuous task! Take another code snippet:
List<String> names; // same names as before names.stream().filter(name->name.equalsIgnoreCase("Anna")).forEach(System.out::println);
Same task, but now it takes only one string of code. What did we do here? We built a pipeline:
- Find all names equal to Anna
- Print each of them
This pipeline consists of an intermediate (fliter()
) and terminate (forEach()
) operations, that we will observe later in this post.
Create streams
Stream is a programming abstraction, so it is not equal to collection, but we create it from collection. These concepts are often mixed by developers, that start with functional Java, but we need to distinguish them. In our example before we create a stream from List
. There are several ways to initialize streams:
From collections
This is an easiest and most obvious one. Java’s Collection
interface has a built-in method stream()
that returns a sequential Stream with this collection as its source. Take a look on a code snippet below:
List<Person> people; Stream<Person> stream = people.stream(); // do something with stream...
Generating streams
If you don’t have a collection of defined data, you can generate data for stream. This may be useful for experementing with streams API methods. We need to provide a [Supplier]() that is used to generate a random sequence of elements. Method generate
returns an infinite sequential unordered stream. Here is an example:
DoubleStram numbers = Stream.generate(Math::random);
In this case we generate a stream with a random Double value. IntStream
and LongStream
also provide a special method range
that we also can utilize to generate a stream. Take a look on a code snippet below:
IntStream integers = IntStream.range(1,20); integers.forEach(System.out::println); LongStream longs = LongStream.rangeClosed(1,20); longs.forEach(System.out::println);
In both cases we have a range between 1 and 20, but outputs are different. This is due to the fact that range
and rangeClosed
return a range that could contains upper limit number or not. rangeClosed
method returns a range that includes both limits, while range
excludes a second value from results.
ofNullable
Another static method that is used to create streams is ofNullable
. It allows us to create a stream containing a single element or empty one (in case of null
). NB this method was introduced in Java 9.
Find a code below:
Person anna = null; Stream<Person> personStream = Stream.ofNullable(anna);
of
Another worth to look method to create streams is of
. There are two overloaded versions of this method:
- of (T element)
- of (T… elements)
In the first case, it returns a sequential Stream containing a single element T
. In the second one, it returns a sequential ordered stream whose elements are the specified values. NB that second version uses varargs as an argument. This code snippet illustrates this method:
Stream<Car> cars = Stream.of(new Car("tesla"), new Car("skoda"), new Car("toyota"), new Car("mazda")); cars.forEach(System.out::println); Stream<Car> skoda = Stream.of(new Car("skoda")); skoda.forEach(System.out::println);
Using iterate()
Same as ofNullable
, this method was introduced in Java 9. iterate
takes two parameters: an initial value (seed) and UnaryOperator that produces a sequence. The method starts with the seed value and iteratively applies the given function to get the next element. Here is an example:
Stream.iterate(0, i -> i + 2);
Empty stream
Finally, we can always create an empty stream. NB we mentioned ofNullable
method that can return an empty stream, but there is another approach to get explicitly empty stream. empty
method returns an empty sequential Stream:
Stream<Double> empty = Stream.empty();
What about Builder?
We explored static methods that are used to create streams. But despite them, there is another way to do it: use Builder. Stream.Builder
allows the creation of streams by generating elements individually and adding them to builder without temporary collections or buffers. Let have a look on it:
// 1. create builder Stream.Builder<String> builder = Stream.builder; // 2. create stream Stream<String> names = builder.add("anna").add("bob") .add("carolina").add("david") .build();
Builder is an another approach to build streams. We initialize a Stream.Builder
instance and then, using add
method populate it with values. Finally, we transform Builder
to Stream
by build
method.
Assemble a pipeline
We took a broad introduction to the subject of stream creation and observed key ways to do it. Now, as we obtained a stream instance we can asseble a pipeline in order to do something useful with the stream. From technical point of view, a pipeline consists of a source (Collection, generator function); followed by zero or more intermediate operations and a terminal operation. The graph below represents a concept of pipeline:

In this section we briefly explore role of intermediate and terminal operations and observe most notable of them.
Intermediate operations
Intermediate operations return new stream and are lazy. Their laziness means that the actual computation on the source data is performed only after the terminal operation is invoked, and source elements are consumed only as needed. We can chain multiple intermediate operations, as each returns a new Stream
object. Take a look on the graph below:

Let now have a quick look on most used intermediate operations.
Filter
In the beginning of the post we already used this operation in order to filter a collection and find matching names. In a nutshell, it returns a new stream consisting of the elements that match the given condition. This method accepts a predicate that specifies a condition.
names.stream().filter(name->name.equalsIgnoreCase("Joe"));
In this code we use filter
to find only names that match Joe. As a result, we will obtain a new stream with only Joes.
Map
There are several map operations, and I decided to group them together under one header. Let start with general map
method. It returns a new stream consisting of the results of applying the mapper function to the elements of the stream. Here is an example code:
Stream.of("anna", "benjamin", "carol", "david", "eliska", "frank") .map(String::toUpperCase) .forEach(System.out::println);
There we also have a source data that is a list of names. We apply mapping function to transform names into UPPERCASE STRINGS. In all cases, mapper is a Function that accepts one argument and produces a result. There other, specific mapping operations:
mapToInt
= produces an IntStream consisting of the results of applying the given mapper functionmapToDouble
= produces a DoubleStream consisting of the results of applying the given mapper functionmapToLong
= produces a LongStream consisting of the results of applying the given mapper function
Distinct
Another notable intermediate operation in Java Stream API is distinct
. It produces a stream of distinct (unique) elements from the data. From technical point of view, distinct
method works with equals
of enitites in order to avoid duplicates. For ordered streams, the selection of distinct elements is stable, while for unordered ones, Java provides no stability guarantees.
List<Integer> numbers = Arrays.asList(1, 1, 2, 3, 3, 4, 5, 5); numbers.stream().distinct().forEach(System.out::println);
That is how this method works with numbers. In your custom entites, as it was mentioned you have to override equals
and hashCode
in order to distinct unique elements. I advice you to go read about overriding hashCode and equals before you will do this.
Sort
Sorting is an another important task that we have to perform with streams. sorted
method is an intermediate operation that provides a stream consisting of the elements of this stream, sorted according to natural order. Take a look on a code snippet below:
List<Integer> numbers = Arrays.asList(-9, -18, 0, 25, 4); numbers.stream().sorted().forEach(System.out::println);
Again, this is how sorting works with numbers. With custom entites you need to implement Comparable
, otherwise, ClassCastException
will be thrown when terminal operation executes. If you do not implement this [marker interface]() you may use an overloaded sorted
version that accepts Comparator
as an argument:
Stream.of("barbora", "daria", "cristopher", "adam", "fritz") .sorted((s1, s2) -> { return s1.compareTo(s2); }).forEach(System.out::println);
While
These two methods were also added since Java 9 release: dropWhile
and takeWhile
. Both are intermediate operations that accepts predicate with condition.
dropWhile
= produces a stream consisting of the remaining elements of this stream after dropping the longest prefix of elements that match the given predicate.takeWhile
= produces a stream consisting of the longest prefix of elements taken from this stream that match the given predicate.
NB both works with ordered streams.
Take a look on example code snippet below:
Set<Integer> numbers = Set.of(1,2,3,4,5,6,7,8); numbers.stream() .takeWhile(x-> x < 5) .forEach(System.out::println);
Limit
Last intermediate operation that we will observe in this post is limit
. It produces a stream consisting of the elements, limited to be no longer than specified length. This method accepts one argument – long
value that represents a required length.
List<Integer> numbers = Arrays.asList(-9, -18, 0, 12, -5, 92, 13, 50, -75, 25, 4); numbers.stream().sorted().limit(5).forEach(System.out::println);
Terminal operations
The other group of operations is called terminal operations. Compare to intermediate operations, there is only one terminal operation that is executed on stream, because after it will be performed, the stream pipeline is consumed, and can no longer be used. Terminal operations produces some result, not streams:

There are several notable terminal operations that we will explore here.
For each
We used this operation in most examples before. This method accepts Consumer
function that defines an action to perform on each element of the stream. You remember, that in the beginning of the post we compared two ways of doing this task:
List<String> names = Arrays.asList("Anna", "Bob", "Carolina", "Denis", "Anna", "Jack", "Marketa", "Simon", "Anna"); for (String name: names){ if (name.equalsIgnoreCase("Anna")){ System.out.println(name); } } names.stream().filter(name->name.equalsIgnoreCase("Anna")).forEach(System.out::println);
We also used here method reference to make code shorter and readable. In a full way it will look like this:
stream.forEach(name->System.out.println(name));
NB that for parallel stream pipelines, this operation does not guarantee to respect the encounter order of the stream, as doing so would sacrifice the benefit of parallelism. For any given element, the action may be performed at whatever time and in whatever thread the library chooses. If the action accesses shared state, it is responsible for providing the required synchronization.
Collect
The previous terminal operation has no return: it consumes data, but does not provide something back. However, often we need to perform some stream operation on collection and then get changed collection back. In these situations we use collect
method. It does a mutable reduction operation on the elements of this stream using collector.
There are two overloaded versions of collect
method: one returns a single result, while another one returns a collection. Let have detailed look:
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5); List<Integer> result = numbers.collect(Collectors.toList());
In this code snippet we use built-in Collectors method to collect stream to list. There are other useful methods that Java provides to us out of the box:
Collectors.toMap
Collectors.toSet
Find
Finally there are operations that return Optional object. I group them together, while they are separate methods. Let list them first:
findAny
findFirst
Both of them do not have any arguments, so you may ask a very reasonable question: how do they actually find data?. These methods work in combination with filter
, that we described earlier. Take a look on example:
List<String> names = Arrays.asList("anna", "barbora", "andrew", "benjamin", "carol"); Optional<String> anna = names.stream().filter(name->name.equalsIgnoreCase("anna")).findFirst(); if (anna.isPresent){ System.out.println("Anna is here!"); } else { System.out.println("No Anna there"); }
Here we use findFirst
in combination with filter
to find a matching result. However, this is a very artificial example: usually we don’t do this, but filter by some pattern:
// names list names.stream().filter(name->name.startsWith("A")).findAny().ifPresent(System.out::println);
In both cases we got Anna. What is a difference between these two methods? As names imply, findFirst
= returns matching element first occured. In our case they are both Anna. findAny
returns any matching element, that can be first or can be not: behavior of this operation is explicitly nondeterministic; it is free to select any element in the stream.
We did a comprehensive review of Java Stream and described most notable methods every developer should know (also this is not complete list, feel free to explore Javadoc). Stream is a concept, borrowed from functional programming languages and better works with other Java functional tools, like optionals. Many developers, however, find them not enough powerful. As alternative to built-in Java tools, we can use Vavr library. In previous parts of this trilogy, I already described Option and Try classes. Now, let finish this tour with Vavr Stream.
Vavr Streams vs. Java Streams
Stream in Vavr is closely connected with collections and is defined in io.vavr.collection
package. Vavr library defines stream as a lazy sequence of elements which may be infinitely long. Vavr streams are bit different from their Java analogs and are built from head element and lazy evauluated tail stream:

Now, let have a more closer look on Vavr streams.
Creating Vavr Stream
There are also various approaches to create streams in Vavr. As in vanila Java we can divide them in several groups:
- Create from single object
- Create from collection
- Create from range
- Create empty stream
Let observe them.
From single object
As in Java we can create stream from only one object as source. of
method can accept single argument:
Stream<Integer> two = Stream.of(2); Stream<Integer> numbers = Stream.of(1,3,5,7,9);
From collection
You can also initialize stream from existing collections using ofAll
method:
List<Intger> list = List.of(1,3,5,7,9); Stream<Integer> numbers = Stream.ofAll(list);
From range
Same as bulit-in Java streams, Vavr streams can have generated data from the specified range of numbers. Take a look on this code snippet:
Stream<Integer> numbers = Stream.range(0,10); Strean<Integer> closed = Stream.rangeClosed(0,10);
Both of these methods create streams with data source in range started from 0. However, first method excludes right value and generate stream with numbers up to 9. Second method includes both limits and generate a stream with numbers 0-10.
Create empty stream
Finally, there is a way to obtain stream with empty data source, likewise it is done in Java. Vavr has static method empty
:
Stream<Integer> empty = Stream.empty();
Vavr Stream operations
Comparing to Java Stream, Vavr does not have a concept of pipelines and therefore does not divide stream operations into intermediate and terminal ones. This section will describe most important operations, that Vavr Stream offers to developers. NB in terms of Java (forgive me, Vavr creators for this analogy, but this is Java vs. Vavr triology!) they are all intermediate, because return streams as a result.
Append
First method we will investigate is append
method. There are two methods in this block: first appends new element to the stream, and second one – called appendAll
adds elements of collections to the stream. Take a look at this example:
Stream<Integer> stream = Stream.rangeClosed(0,10); stream.add(11); List<Integer> numbers = Arrays.asList(12,13,14,15,16); stream.addAll(numbers);
Cycle
Vavr supplies to us a very interesting method cycle
. It repeats elements of stream. There are two overloaded versions: one – without any arguments – repeats elements of the stream infinitely. Another version accepts integer value count
that corresponds to the number of times you need to repeat eleemnts of the stream.
Stream<Integer> numbers = Stream.of(1,2,3,4,5); numbers.cycle(3); // repeat elements 3 times
Distinct
If you need to remove duplicate elements, you can utilize distinct
method. This block corresponds to two versions: plain distinct
method remove duplicates based on a logic of equals
method of elements. Another possibility is to use distinctBy
method that is much flexible as it allows to supply Comparator
as an argument.
// step 1. Define entity class Elephant{ private String name; // .. @Override public boolean equals(Object object){ if (object !instanceof Elephant) return false; Elephant compared = (Elephant) object; return this.name.equalsIgnoreCase(compared.getName()); } } // step 2. Create stream Stream<Elephant> elephants = Stream.of(new Elephant("John"), new Elephant("Carol"), new Elephant("John"), new Elephant("Marta")); // step 3. remove duplicates elephants.distinct();
Drop
This block contains a number of methods used to drop elements of stream. We can combine them into three groups:
drop
accepts integer value as an argument that corresponds to the number of elements to drop. This method drops the first n elements of this or all elements, if this length < n.dropUntil
accepts Predicate argument with a condition tested subsequently for this elements. This method drops elements until the predicate holds for the current element.dropWhile
also accepts a condition in a form of Predicate function. Drops elements while the predicate holds for the current element.
Here is an example with elephants:
Stream<Elephant> elephants = Stream.of(new Elephant("John"), new Elephant("Carol"), new Elephant("John"), new Elephant("Simon"), new Elephant("Marta")); elephants.dropUntil(e -> e.getName().equalsIgnoreCase("Simon"));
Reverse
If you need to reverse the order of elements in your stream, you can use reverse
method:
Stream<Integer> numbers = Stream.range(1,11); numbers.reverse();
Slice
Another method to manipulate elements is slice
. This method accepts two arguments – beginning index and end index. The slice begins with the element at the specified beginning index and extends to the element at index = end index - 1
.
Stream<Integer> numbers = Stream.of(1,2,3,4); numbers.slice(1,3); // result = (2,3)
Conclusion
This post explored to a subject of streams – a concept that came from functional programming languages. Technically, stream is a sequence of elements supporting sequential and parallel aggregate operations. Java started to include Streams API as a part of JDK 8 and has improved them in subsequent releases. Also, Java streams are not only ones. Vavr library that offers additions to functional Java also supplies own streams that are a bit different from vanilla Java. During this post we observed core tasks connected to Java streams: creation, building pipelines. Then, we also discussed about creation of Vavr streams and most notable operations with them. We will later also continue to work with functional Java concepts vs. Vavr. Meanwhile, don’t hesitate to drop me a message if you have any questions. Have a nice day!
References
- Benjamin Winterberg Java 8 Stream Tutorial (2014) read here
- Grzegorz Piwowarek. Vavr Collections and Java Stream API Collectors (2017) 4Comprehension, read here
- Nicolais Frankel Java streams and state (2019) Java Geek, read here
- Saeed Zarinfam Streams API New Features after Java 8 (2019) ITNext, read here