Stream API with examples in Java

Amine Daoud
5 min readSep 10, 2022

Introduction

In this article, we will explore the Stream API that was introduced in Java 8 and it’s one of the main functional programming APIs in Java.

A stream has no storage, it’s not a data structure as many might think, it just uses a source like a collection and transform it through a series of multiple operations to a destination. In these operations, there could be multiple intermediate operations and only one terminal operation that consumes the stream such as foreach and collect.

The stream doesn’t change the source and so it produces a new result.

After a stream is consumed by a terminal operation, it cannot be consumed again and a new stream shall be created otherwise you will get java.lang.IllegalStateException: stream has already been operated upon or closed.

Most Stream operations are always implemented in a lazy way in order to seek performance by aggregating multiple operations together.

As a example, if you want to filter names starting with “M” then return the first element. The filter operation will just find the first element starting with ‘M’ and the stream will be consumed immediately. So, filter and findFirst operations are combined together. Below is an example that demonstrates this.

Stream<String> st = Stream.of("Marc", "Helen", "Mario", "Sarah");
Optional<String> res = st.filter(s -> s.startsWith("M")).peek(s -> System.out.println("debug " + s)).findFirst();
System.out.println(res.get());

If we add a peek between the filter and findFirst for debugging purposes, we can that filter has been applied only on the first name starting with “M”.

debug Marc
Marc

Stream operations

1. map

map is one of the most common stream operations. It’s an intermediate operation and it returns a new Stream after applying a function to each element of the stream.

In the below example, we want to transform words in uppercase. collect is used as a terminal operation to indicate the structure that should be returned (in this case a list).

List<String> animals = List.of("Dog", "Rabbit", "Cat", "Mouse", "Bird");
System.out.println(animals.stream().map(String::toUpperCase).collect(toList()));
--------------------------------------------------------------------[DOG, RABBIT, CAT, MOUSE, BIRD]

We can also collect the result to a set or concatenate the list elements like below.

System.out.println(animals.stream().map(String::toUpperCase).collect(joining("|")));--------------------------------------------------------------------DOG|RABBIT|CAT|MOUSE|BIRD

2. forEach

forEach is a terminal operation and it allows to iterate over the elements of a stream and apply a function for each element. In the below case, we want to print each element in the stream:

Stream<String> st = Stream.of("house", "car", "laptop");
st.forEach(System.out::println);
--------------------------------------------------------------------house
car
laptop

3. mapToInt

We can use mapToInt to map a stream to primitive integer values. in the below example, we invoke then a terminal operation sum to calculate the sum of the stream elements.

Stream<String> st = Stream.of("1", "2", "3");
st.mapToInt(Integer::parseInt).sum();
--------------------------------------------------------------------6

4. sorted

sorted is another intermediate operation and used to arrange the order of stream elements following a provided comparator. In the below example, we want to sort the words following the ascendant order which is using Comparator#reverseOrder.

Stream<String> st = Stream.of("house", "car", "laptop");
System.out.println(st.sorted(Comparator.reverseOrder()).collect(toList()));
--------------------------------------------------------------------[laptop, house, car]

5. filter

filter is an intermediate operation which allows to select a subset of elements matching a condition. In the below example, we want to select only strings starting with letter ‘c’:

Stream<String> st = Stream.of("house", "car", "laptop");
System.out.println(st.filter(str -> str.startsWith("c")).collect(toList()));
--------------------------------------------------------------------[car]

6. reduce

reduce is a terminal operation, it allows us to reduce multiple elements to one result following a reduction function like concatenating strings or adding integers.

In the below example, we want to create a string of comma separated words from the stream.

Stream<String> st = Stream.of("house", "car", "laptop");
System.out.println(st.reduce((e1, e2) -> e1 + ", " + e2));
--------------------------------------------------------------------Optional[house, car, laptop]

7. distinct, limit , skip

These operations are intermediate.

distinct allows to return distinct elements of the list by comparing all the elements in the list using equals and removing duplicates.

Regarding limit, as the name suggests you can limit the results the first n elements of the stream.

skip will let you skip n first elements from the stream and display the remaining elements.

List<Integer> ints = List.of(2, 34,2, 5 , 7, 7, 7, 34, 17);System.out.println(ints.stream().distinct().collect(toList()));
System.out.println(ints.stream().limit(2).collect(toList()));
System.out.println(ints.stream().skip(5).collect(toList()));
--------------------------------------------------------------------[2, 34, 5, 7, 17]
[2, 34]
[7, 7, 34, 17]

8. min , max

min and max are terminal operations which allows to find the minimum and maximum values in a stream using a comparator. Both min and max returns an Optional as a result.

List<Integer> ints = List.of(2, 34,2, 5 , 7, 7, 7, 34, 17);System.out.println(ints.stream().min(Comparator.reverseOrder()));
System.out.println(ints.stream().max(Comparator.naturalOrder()));
--------------------------------------------------------------------Optional[34]
Optional[34]

9. flatMap

flatMap is an intermediate operation. It’s useful when we operate on list of lists and want to access all the elements by flattening these lists.

In the below example, let’s consider a list of a list of integers. We want to find all the integers that are greater than 3 so we transform the listOfLists to a stream and then use flatMap method to flatten the lists and finally filter the matching elements using the condition.

List<List<String>> listOfLists = List.of(List.of("1", "2"), List.of("3", "4"), List.of("5", "6"));
System.out.println(listOfLists.stream().flatMap(Collection::stream).filter(x-> Integer.parseInt(x) > 3).collect(toList()));
--------------------------------------------------------------------[4, 5, 6]

10. allMatch, anyMatch, noneMatch

All these operations are terminal.

allMatch returns true if all elements in stream match a condition.

anyMatch returns true if at least one element in stream match a condition.

noneMatch returns true if all elements in the stream do not match the condition.

List<Integer> ints = List.of(2, 34,2, 5 , 7, 7, 7, 34, 17);System.out.println(ints.stream().allMatch(x -> x>1));
System.out.println(ints.stream().anyMatch(x -> x==1));
System.out.println(ints.stream().noneMatch(x -> x==17));
--------------------------------------------------------------------true
false
false

11. peek

peek is an intermediate operation. It’s usually used to debug the elements of the stream during intermediate operations.

Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(toList());
--------------------------------------------------------------------Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR
[THREE, FOUR]

12. Collect using groupingBy

GroupingBy is useful when we want to group the elements of the stream using a property.

In this example, we want to group the words by their frequency and return a frequency list corresponding to the distinct words and displayed in the natural order of the words.

List<String> words = Arrays.asList("dog", "cat", "hello", "cat", "car", "hello", "hello");
Map<String, Long> res = words.stream().sorted().collect(
groupingBy(Function.identity(), LinkedHashMap::new, counting()));
System.out.println(res);
--------------------------------------------------------------------{car=1, cat=2, dog=1, hello=3}

13. Collect using partioningBy

partioningBy allows to partition the results in a map with boolean keys. The values are a list of elements matching the condition for the true key and the false key is mapped to the elements not matching.

List<String> st = List.of("cat", "fish", "dog", "carl", "fur");
System.out.println(st.stream().collect(partitioningBy(str-> str.startsWith("d"))));
--------------------------------------------------------------------{false=[cat, fish, carl, fur], true=[dog]}

--

--