Stream API in Java 8

Prafull R
13 min readMar 16, 2024

--

Since the launch of Java 8, we’ve been introduced with a very helpful API — Stream API. Prior to this API, the dealing with large data sets was quite cumbersome and the code used to look ugly and unreadable hence un-manageable.

Stream API introduces the package java.util.stream which contains loads of methods to play with. The central API interface is Stream<T>. These methods are categorized in two types based on the operations —

1. Intermediate Operations — These types of methods are always lazy and return another Stream thus can be chained together.
2. Terminal Operations — These types of methods return non-Stream results like, primitive data or Collection type data or no data at all.

Java 8 Streams API Operations visualization by Prafull

Before diving deep into these operations, it’s suggested to know the basics of Java 8 (Optional, Lambda & Method references). Now, since you’ve the working knowledge of these, let’s dive deep into these methods with some examples to understand their usage better.

1. Intermediate Operations

1.1. filter() — This method filters Streams of data and returns another stream based on the stateless predicate (or conditions) being provided to it.

List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 2);
numbers.stream()
.filter(x -> x % 2 == 0)
.forEach(System.out::println);
Output: 
2
4
2

In the above example, we’ve taken a List of Integers with some random integers and then with the help of Stream API filter() method, we’re trying to filter the even numbers from the list.
The filter() method takes the predicate as lambda expression and returns another stream of Integers which are only even.
Then with the help of terminal forEach() method, we’ve just printed the output.

1.2. map() — This is another data manipulation method which is used to convert the streams of data from one type to another. This method accepts Function mapper object as parameter <R> Stream<R> map(Function<? super T, ? extends R> mapper). Once the mapping is completed, it returns another stream as output.


List<Integer> numbers = Arrays.asList(2, 3, 4);
numbers.stream()
.map(x -> x * 2)
.forEach(System.out::println);
Output: 
4
6
8

In the above example, we’ve taken a List of Integers with some random integers. Now with the help of Stream API map() method and lambda expression as an argument to the map() method, we’re multiplying each integer with 2.
This returns another stream in which all the integers are multiplied with 2, thus data is manipulated.
Then with the help of terminal forEach() method, we’ve just printed the output.

1.3. flatMap() — This is another data manipulation method but as the name says, it’s a combination of two methods — flat & map. It means, this intermediate method will first map the stream of data and then flatten it i.e., it’ll merge multiple collections/arrays into one.

List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(4, 5, 6);
List<Integer> numbers3 = Arrays.asList(7, 8, 9);
List<List<Integer>> listOfNumbers = Arrays.asList(numbers1, numbers2, numbers3);
List <Integer> flattenedList = listOfNumbers.stream()
.flatMap(List::stream) // flattening step
.toList();
System.out.print(flattenedList);
Output:
[1, 2, 3, 4, 5, 6, 7, 8, 9]

In the above example we’ve taken 3 Lists of Integer and then created a new List of Lists of those Integers which when printed would have given output as [[1, 2, 3], [4, 5, 6], [7, 8, 9]].
Now with the help of Stream API flatMap() method and lambda expression as an argument to flatMap(), we’re flattening the List of List of Integers and then print out the newly flattened list.

1.4. distinct() — This stateful method removes duplicate elements from a Streams of data and return another Streams. It means only unique elements will be retained in the Stream.

List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 2, 5, 1);
numbers.stream()
.distinct()
.forEach(System.out::println);
Output:
2
3
4
5
1

In the above example, we’ve taken a List of Integers with duplicate elements. Now with the help of Stream API distinct() method, we’ve only retained the unique elements in the List.
This returns another Stream which doesn’t contain any duplicate elements.
Then with the help of terminal forEach() method, we’ve just printed the output.

1.5. sorted() — As the name says, this stateful method helps in sorting the Stream. The Stream interface provides two methods for sorting:
a. sorted() — It sorts the Stream in default order.
b. sorted(Comparator) — It sorts the Stream based on given Comparator.

List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 1);
numbers.stream()
.sorted()
.forEach(System.out::println);
Output:
1
2
3
4
5

In the above example, we’ve taken an unordered List of Integers. Now with the help of Stream API sorted() method, we’ve sorted the List of Integers.
This returns another Stream which is sorted in default order. Then with the help of terminal forEach() method, we’ve just printed the output.

List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 1);
numbers.stream()
.sorted(Comparator.reverseOrder())
.forEach(System.out::println);
Output:
5
4
3
2
1

In the above example, we’ve taken an unordered List of Integers. Now with the help of Comparator.reverseOrder() as parameter in Stream API sorted(Comparator) method, we’ve sorted the List of Integers in reverse order.
This returns another Stream which is sorted in reverse order. Then with the help of terminal forEach() method, we’ve just printed the output.

List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 1);
numbers.stream()
.sorted((i1, i2) -> i1.compareTo(i2))
.forEach(System.out::println);
Output:
1
2
3
4
5

In the above example, we’ve taken an unordered List of Integers. Now with the help of Stream API sorted() method and lambda expression as an argument to the sorted() method, we’ve sorted the List of Integers in ascending order.
This returns another Stream which is sorted in ascending order. Then with the help of terminal forEach() method, we’ve just printed the output.

1.6. peek() — This method is just helpful in debugging where we want to see the elements of Stream as they flow past a certain point in pipeline. This method returns a new Stream consisting of all elements from the original Stream after applying given Consumer action as parameter in Stream<T> peek(Consumer<? super T> action).

List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 2);
List<Integer> newNumbers = numbers.stream()
.peek(System.out::println)
.toList();
System.out.println(newNumbers);
Output:
2
3
4
5
2
[2, 3, 4, 5, 2]

In the above example, we’ve created a List of Integer. Now with the help of Stream API peek() method, we’re visualising how the stream operations behave by applying a Consumer action as printing each element in new line.
This method returns another Stream which is then collected as a List using toList() method. Then we’ve just printed this newly collected list in a new line.

1.7. limit() — As the name says, this short-circuiting method limits the number of elements being returned from a Stream i.e., the count of elements shouldn’t be greater than maxSize being provided.

List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 2);
numbers.stream()
.limit(3)
.forEach(System.out::println);
Output:
2
3
4

In the above example, we’ve created a List of Integer. Now with the help of Stream API limit() method, we’ve limited the number of elements of the output to 3 which is being provided as an argument to the limit(long maxLength) method.
This method returns another Stream which is limited to the desired count in the way it encounters the Stream. Then with the help of terminal forEach() method, we’ve just printed the output.

1.8. skip() — As the name says, this stateful method skips the first n elements from the given Stream.

List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 2);
numbers.stream()
.skip(3)
.forEach(System.out::println);
Output:
5
2

In the above example, we’ve created a List of Integer. Now with the help of Stream API skip() method, we’ve skipped the first 3 elements which is being provided as an argument to the skip(long n) method.
This method returns another Stream which has skipped the desired number of elements from the given Stream. Then with the help of terminal forEach() method, we’ve just printed the output.

2. Terminal Operations

2.1. forEach() — We’ve been using this method in all above examples and the usage of this terminal method is to iterate over all the elements of Stream. This is an optimised way of writing for-each loop.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);
numbers.stream()
.sorted()
.forEach(System.out::println);
Output:
2
3
4
5

In the above example, we’ve created a List of Integer. Now with the help of Stream API sorted() method, we’ve sorted the Stream in natural order. Then with the help of terminal forEach() method, we’ve just printed the output by iterating the stream and performing the print in new line as Consumer action on each element.

2.2. forEachOrdered() — This method is a combination of Stream API forEach() & sorted(Consumer<? super T> action). This method is used to iterate all the elements of Stream and then perform Consumer action on each of them.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);
numbers.stream()
.forEachOrdered(System.out::println);
Output:
2
3
4
5

In the above example, we’ve created a List of Integer. Now with the help of Stream API forEachOrdered() method we’ve printed each element as a Consumer action.

2.3. toArray() — As the name says, this terminal method converts the Stream to an array.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);
Integer[] intArr = numbers.stream().toArray(Integer[]::new);
System.out.println(Arrays.toString(intArr));
Output:
[5, 3, 2, 4]

In the above example, we’ve created a List of Integer. Now with the help of Stream API toArray() method, we’ve converted the List of Integer to an array of Integer.

Now, we’ve just printed the intArr as output by first converting it into Array of String using toString() method of Arrays.

2.4. reduce() — As the name says, this terminal operation is used when we need to reduce the Stream in a single resultant value e.g., maximum, minimum sum, string concatenation, etc.

List<String> strings = Arrays.asList(“My”, “name”, “is”, “Prafull”);

// Finding longest string from the List
Optional<String> longestString = strings.stream()
.reduce((str1, str2)
-> str1.length() > str2.length()
? str1 : str2);
longestString.ifPresent(System.out::println);

// String concatination
Optional<String> strConcat = strings.stream()
.reduce((str1, str2)
-> str1 + “ “ + str2);
strConcat.ifPresent(System.out::println);
Output:
Prafull
My name is Prafull

In the above example, we’ve created a List of Strings. Now with the help of Stream API reduce() method, we’re performing different operations. As you can see, the results is an Optional with single result value and so with the help of ifPresent() method of Optional, we’re checking if the object has value and if it has, then we’re just printing the results.

2.5. min() — This terminal method is used to select the minimum/smallest element in the Stream. This method accepts a non-interfering, stateless Comparator to compare the elements of the Stream and returns an Optional.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);
Optional<Integer> minNum = numbers.stream()
.min((num1, num2) -> num1.compareTo(num2));
System.out.println(minNum.get());
Output:
2

In the above example, we’ve created a List of Integer. Now with the help of Stream API min(), we’re trying to find out the minimum Integer from the List by passing the compareTo() method of Integer as lambda expression in the parameter of min(Comparator<? super T> comparator) method and storing it in an Optional<Integer> variable.

Now, we’ve printed the output by getting the value from the minNum variable using get() method of Optional.

A custom Comparator can also be used to compare the values and find out the minimum.

2.6. max() — This terminal method is similar to min() but is used to select the maximum/largest element in the Stream. This method accepts a non-interfering, stateless Comparator to compare the elements of the Stream and returns an Optional.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);
Optional<Integer> maxNum = numbers.stream()
.max((num1, num2) -> num1.compareTo(num2));
System.out.println(maxNum.get());
Output:
5

In the above example, we’ve created a List of Integer. Now with the help of Stream API max(), we’re trying to find out the maximum Integer from the List by passing the compareTo() method of Integer as lambda expression in the parameter of min(Comparator<? super T> comparator) method and storing it in an Optional<Integer> variable.

Now, we’ve printed the output by getting the value from the maxNum variable using get() method of Optional.

A custom Comparator can also be used to compare the values and find out the maximum.

2.7. count() — As the name says, this terminal operation is used to count the number of matching items in the Stream.

We can use the following methods as both results the same long value.

a. Stream.count()

b. Stream.collect(Collectors.counting())

long countStr = Stream.of(“hi”, “I”, “am”, “Prafull”).count();
System.out.println(countStr);

long countInt = Stream.of(1,2,3,4,5).collect(Collectors.counting());
System.out.println(countInt);
Output:
4
5

In the above example, we’ve created two Stream. First is Stream of String and second is Stream of Integer.

Now with the help of Stream API count() method & collect(Collectors.counting()), we’re trying to count the matching items in the Stream and store it in a variable of long type. Then we’re just printing the output.

2.8. anyMatch() — As the name says, this terminal short-circuit operation is used to check whether the Stream contains at least one element which satisfies the given Predicate as argument to anyMatch(Predicate<? super T> predicate) method which returns a boolean.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);
boolean result = numbers.stream()
.anyMatch(num -> num.contains(2));
System.out.println(result);
Output:
true

In the above example, we’ve created a List of Integer. Now with the help of Stream API anyMatch() method, we’re trying to find if the list numbers contains the Integer 2.

Since, the list contains the integer and so when we’re trying to print the output, it prints true.

2.9. allMatch() – As the name says, this terminal short-circuit operation is used to check whether all the elements in the Stream satisfies the given Predicate as argument to allMatch(Predicate<? super T> predicate) method which returns a boolean.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);
boolean result = numbers.stream()
.allMatch(num -> num > 0);
System.out.println(result);
Output:
true

In the above example, we’ve created a List of Integer. Now with the help of Stream API allMatch() method, we’re trying to find if all the integers in the list numbers are greater than zero (0).

Since, the list contains all the integers greater than zero and so when we’re trying to print the output, it prints true.

2.10. noneMatch() – As the name says, this terminal short-circuit operation is quite opposite of allMatch(). This is used to check if none of the element in the Stream satisfies the given Predicate as argument to noneMatch(Predicate<? super T> predicate) method which returns a boolean.

This method returns true if none of the elements matches or if the stream is empty and returns false if at least one element matches.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);
boolean result = numbers.stream()
.noneMatch(num -> num < 0);
System.out.println(result);
Output:
true

In the above example, we’ve created a List of Integer. Now with the help of Stream API allMatch() method, we’re trying to find if all the integers in the list numbers are smaller than zero (0).

Since, the list contains all the integers smaller than zero and so when we’re trying to print the output, it prints true.

2.11. findFirst() — This operation is used to find the first encountered element in the Stream as an Optional. In case of a stream has:

a. defined encounter order – first element in encounter order in stream.

b. no encounter order – any element may be returned.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);

// Sequential stream
Optional<Integer> result1 = numbers.stream()
.findFirst();

// Parallel stream
Optional<Integer> result2 = numbers.stream()
.parallel()
.findFirst();
System.out.println(result1);
System.out.println(result2);
Output:
5
5

In the above example, we’ve created a List of Integer. Now with the help of Stream API findFirst() method, we’re trying to find the first encountered element.

For the first Integer result1, we’re trying to fetch the first encountered element in an Un-paralleled Stream but for the second Integer result2, we’re trying to fetch the first encountered element in a Parallel Stream.

For both of the above cases, the output will be the first element and so while printing both the results, we get 5 as output.

2.12. findAny() — This operation is used to find any encountered element in the Stream as an Optional. This method has been introduced to gain performance in case of Parallel Stream only. In case of a stream has:

a. defined encounter order – any element in encounter order in stream.

b. no encounter order – any element may be returned.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);

// Sequential stream
numbers.stream()
.findAny()
.ifPresent(System.out::println);

// Parallel stream
numbers.stream()
.parallel()
.findAny()
.ifPresent(System.out::println);
Output:
5
2

In the above example, we’ve created a List of Integer. Now with the help of Stream API findAny() method, we’re trying to find the first encountered element.

For the first Integer, we’re trying to fetch and print any encountered element in an Un-paralleled Stream but for the second Integer, we’re trying to fetch the any encountered element in a Parallel Stream.

In case of Un-paralleled Stream for both findFirst() and findAny() we’re getting the first element in return but findAny() doesn’t always guarantee this behaviour.

2.13. toList() – This terminal method is used to perform a mutable reduction operation on the Stream elements. The syntax of this method is <R, A> R stream.collect(Collector<? super T,A,R> collector) and so it can be used to perform several types of reduction operations e.g., find the min or max, calculate sum, string concatenation, collecting Stream into a new List or Set.

List<Integer> numbers = Arrays.asList(5, 3, 2, 4);

// Find max number
Integer max = numbers.stream().collect(maxBy(Integer::compare)).get();
System.out.println(max);

// Calculate sum of all numbers
Integer sum = numbers.stream().collect(summingInt(i -> i));
System.out.println(sum);

// Counting the numbers
Integer count = numbers.stream().collect(counting());
System.out.println(count);
Output:
5
14
4

In the above example, we’ve created a List of Integer. Now with the help of Stream API collect() method, we’re performing different operations with the help of methods available in Collectors interface. Then we’re printing the results.

2.14. toList() — This method has been introduced in Java 16 in the Stream API to collect the Stream elements in a List. This method creates an un-mutable List which allows null elements.

Previously we were using below methods to accumulate Stream elements into a List:

a. Stream.collect(Collection.toList()) – Since Java 8 and creates a mutable List which allows null elements.

b. Stream.collect(Collectors.toUnmodifiableList()) – Since Java 10 and creates an un-mutable List which doesn’t allow null elements and the List is unmodifiable.

The advantage of using Stream.toList() is that it uses less memory as it implements Collector interface independently and makes the code neat & concise.

Stream<Integer> intStream = IntStream.of(5, 3, 2, 4);
List<Integer> intList = numbers.toList();

In the above example, we’ve created a Stream of Integers and now with the help of Stream API toList() method, we’re collecting all the elements on intStream in intList.

Now, we’ve mastered the Stream API and different types of operations available with it. As you’ve seen how useful this API is which makes our code pretty neat, concise and also makes the processing faster.

While applying lambda functions with this API, we’re getting our desired results in a single line code rather than writing multi-line code.

--

--

No responses yet