paint-brush
An In-Depth Guide on Java Streams in Java 8by@sandeepmishratech
2,232 reads
2,232 reads

An In-Depth Guide on Java Streams in Java 8

by Sandeep MishraNovember 9th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Java Streams, introduced as part of Java 8, is used to work with collections of data. It is not a data structure in itself but can be used for taking input from other data structures with the help of sequencing and pipelining to get the final output.
featured image - An In-Depth Guide on Java Streams in Java 8
Sandeep Mishra HackerNoon profile picture

Introduction to Java Streams in Java 8

Java Streams, introduced as part of Java 8, is used to work with collections of data. It is not a data structure in itself but can be used for taking input from other data structures with the help of sequencing and pipelining to get the final output.


Since it is not a separate data structure, it never really alters the data source either. Thus, Java streams in Java 8 might be said to be having the following features:


  1. Java streams can be used with the help of the “java.util.stream” package in Java. This can be imported into a script using the statement:


    import java.util.stream.* ;

    Using this, we can also implement multiple in-built functions on Java streams with ease.


  1. A Java stream is not a data structure. It can take input from data collections, such as Collections and Arrays in Java.


  2. A Java stream does not involve a change in the input data structure.


  3. A Java stream does not alter the source. Instead, it generates output by pipelining methods accordingly.


  4. Java streams undergo intermediate and terminal operations, which we will discuss in further sections.


  5. In Java streams, intermediate operations are pipelined and lazily evaluated. They are terminated by terminal functions. This forms the basic format of using a Java stream.


In the next section, we will take a look at various ways used in Java 8 to create Java streams as needed.

Creating Java Streams in Java 8

Java Streams can be created using multiple ways. Some of them are listed in this section as follows:


  1. Creating an empty stream using Stream.empty() method


One might create an empty stream to be used during later stages of the code. Using “Stream.empty()” method, an empty stream containing no values would be generated. This empty stream can come in handy when we want to omit a Null Pointer Exception in the runtime. The following command can be used for the same:


Stream<String> str = Stream.empty();


The above statement would generate an empty stream named “str” without any elements inside it. This can be verified by checking the count or size of the stream using str.count() term. For example,


System.out.println(str.count());**
**

This print statement would output “0” as a result.


  1. Creating stream using Stream.builder() method with Stream.Builder instance

**
**

We can also use Stream Builder to create a stream with the help of a builder.  A builder is basically a pattern for the construction of objects one step at a time. Let us see how we can create an instance of a stream using Stream Builder.


Stream.Builder<Integer> numBuilder = Stream.builder();

numBuilder.add(1).add(2).add( 3);

Stream<Integer> numStream = numBuilder.build();


Using this would build a stream named “numStream” containing some “int”  elements. This is done quickly with the help of the Stream.Builder instance “numBuilder” created first.


  1. Creating stream with specified values using Stream.of() method


Another method of creating a stream is with the help of “Stream.of()” method. This is a simple way of creating a stream with specified values. It declares as well as initializes the stream. An example of using the “Stream.of()” method for creating a stream is as follows:


Stream<Integer> numStream = Stream.of(1, 2, 3);


This would create a stream containing “int” elements as we had done in the previous method, which involved a “Stream.Builder” instance. Here, we directly created a stream using “Stream.of()” with pre-specified values [1, 2, and 3].


  1. Creating stream from an existing array using Arrays.stream() method


Another common method to create a stream is using the arrays in java. A stream can also be created using the “Arrays.stream()” method. This creates a stream from an existing array. The elements of the array are all converted into stream elements. An example of how this can be done is as follows:


Integer[] arr = {1, 2, 3, 4, 5};

Stream<Integer> numStream = Arrays.stream(arr);


This code would generate a stream “numStream” containing the contents of the array named “arr” which is an integer array.


  1. Combining two existing streams using the Stream.concat() method


Another method that can be used to generate a stream is the “Stream.concat()” method. This method is used to combine two streams to create a singular stream. Both streams are concatenated as ordered. This is to say that the first stream comes first, followed by the second stream in the final stream. An example of such a concatenation is as follows:


Stream<Integer> numStream1 = Stream.of(1, 2, 3, 4, 5);

Stream<Integer> numStream2 = Stream.of(1, 2, 3);

Stream<Integer> combinedStream = Stream.concat( numStream1, numStream2);


The above statement would create a final stream named “combinedStream” containing the elements of the first stream, “numStream1” and the second stream, “numStream2” one after the other.


Type of Operations on Java Streams

As mentioned above, two types of operations can be performed on Java Streams in Java 8. The two broad categories of operations are intermediate operations and terminal operations. Let us take a look at each of them in more detail in this section.


Intermediate Operations: Intermediate operations generate an output stream and are only executed when a terminal operation is encountered. This is to say that intermediate operations are lazily executed and pipelined and can only be terminated by a terminal operation. We will read about lazy evaluation and pipelining in further sections.


Some examples of intermediate operations are the following methods:

filter(), map(), distinct(), peek(), sorted(), and so on.


Terminal Operations: Terminal operations terminate the execution of intermediate operations and also return the final output stream results. Since terminal operations mark the end of the lazy execution and pipelining; a stream cannot be used again once it has undergone a terminal operation.


Some examples of terminal operations are the following methods:

forEach(), collect(), count(), reduce(), and so on.


Java Stream Operations (Examples)

Intermediate Operations

Here are some examples of some intermediate operations that can be applied to Java streams:


1 - filter()

This method is used for filtering out elements from a stream that match a particular Predicate in Java. These filtered elements constitute a new stream. Let us take a look at an example to understand this better.


Code:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> even = numStream.filter(n -> n % 2 == 0) .collect(Collectors.toList()); System.out.println(even);


Output:

[98]


Explanation:

In this example, one can see that the even elements (which are divisible by 2) are filtered using filter() method and stored in an Integer list “numStream” whose contents are printed later. As 98 is the only even integer in the stream, it is printed in the output.


2 - map()

This method is used for producing a new stream by performing mapped functions on the elements of the original input stream. It is possible that the new stream has a different data type.


An example of the same is as follows:


Code:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> d = numStream.map(n -> n*2) .collect(Collectors.toList()); System.out.println(d);


Output:

[86, 130, 2, 196, 126]


Explanation:

Here, we you see that map() method is used to map each element of the stream “numStream” by 2, or, double the value of each stream element. The mapping done here is multiplication by 2. As seen in the output, each of the elements in the stream are doubled successfully.


3 - distinct()

This method is used for extracting only the distinct elements in a stream by filtering out the duplicates. An example of the same is as follows:


Code:

Stream<Integer> numStream = Stream.of(43,65,1,98,63,63,1); List<Integer> numList = numStream.distinct() .collect(Collectors.toList()); System.out.println(numList);


Output:

[43, 65, 1, 98, 63]


Explanation:

In this case, distinct() method is being used on “numStream” to extract all distinct elements in the list “numList” by removing the duplicates from the stream. As seen in the output, there are no duplicates present unlike the input stream which had two duplicates (63 and 1) originally.


4 - peek()


This is used for keeping track of intermediate changes before the execution of the terminal operation. This is to say that peek() can be used to perform an operation on each element of the stream to generate a stream on which further intermediate operations can be performed.


Code:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> nList = numStream.map(n -> n*10) .peek(n->System.out.println("Mapped: "+ n)) .collect(Collectors.toList()); System.out.println(nList);


Output:

Mapped: 430 Mapped: 650 Mapped: 10 Mapped: 980 Mapped: 630 [430, 650, 10, 980, 630]


Explanation:

Here, peek() method is being used to generate intermediate results as the map() method is applied to the stream elements. We can observe here that even before the terminal operation collect() is applied to print the final contents of the list in the following “print” statement, result for each mapping of stream element is printed consecutively beforehand itself.


5 - sorted()

The sorted() method is used for sorting the elements of the stream. By default, it sorts the elements in ascending order. One can also specify a particular order for sorting as a parameter. An example of how this method is implemented is as follows:


Code:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.sorted().forEach(n -> System.out.println(n));


Output:

1 43 63 65 98


Explanation:

Here, sorted() method is being used to sort the stream elements in Ascending order by default (since no specific order has been mentioned). The elements printed in the output can be seen to be ordered in ascending order.


Terminal Operations

Here are some examples of some terminal operations that can be applied to Java streams:


1 - forEach()

forEach() method is used to loop through all the elements of the stream and perform a function on each element one by one. This acts as an alternative to looping statements such as “for,” “while,” etc. An example of the same is as follows:


Code:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.forEach(n -> System.out.println(n));


Output:

43 65 1 98 63


Explanation:

Here, forEach() method is being used to print each element of the stream one by one.


2 - count()


The count() method is used to extract the total number of elements that are present in the stream. This is similar to the size() method that is often used to determine the total number of elements in a collection. An example of using the count() method with Java streams is as follows:


Code:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); System.out.println(numStream.count());


Output:

5


Explanation:

Since the stream “numStream” contains 5 integer elements, the output comes out to be “5” on using the count() method on it.


3 - collect()


The collect() method is used to perform mutable reductions on the stream elements. It can be used to remove content from the stream once the processing is completed. It utilizes the

Collector class in Java for carrying out reductions.


Code:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> odd = numStream.filter(n -> n % 2 == 1) .collect(Collectors.toList()); System.out.println(odd);


Output:

[43, 65, 1, 63]


Explanation:

In this example, all the odd elements(which are not divisible by 2) in the stream are filtered and collected/reduced into a list named “odd.” In the end, the list “odd” is printed.


4 - min() and max()


The min() method, as the name suggests, can be used on a stream to find the minimum element in that stream. Similarly, the max() method can be used to find the maximum element in a stream. Let us try to understand how the two can be used with the help of an example.


Code:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); int smallest = numStream.min((m, n) -> Integer.compare(m, n)).get(); System.out.println("Smallest element: " + smallest);

numStream = Stream.of(43, 65, 1, 98, 63); int largest = numStream.max((m, n) -> Integer.compare(m, n)).get(); System.out.println("Largest element: " + largest);


Output:

Smallest element: 1 Largest element: 98


Explanation:

In this example, we have printed the smallest element in the stream “numStream” using min() method and the largest element using max() method.

Please note that here we have added the elements to the stream “numStream” again before applying max() method. This is because min() is a terminal operation and it destroys the contents of the original stream, returning only the final result (which was integer “smallest” in this case).


5 - findAny() and findFirst()


findAny() returns any element of a stream as an Optional. If the stream is empty, it will return the Optional returned will be empty too.

findFirst() returns the first element of a stream as an Optional. As in the case of findAny() method, findFirst() method also returns an empty Optional if the concerned stream is empty. Let us take a look at the following example based on these methods.



Code:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); Optional<Integer> opt = numStream.findFirst();System.out.println(opt); numStream = Stream.empty(); opt = numStream.findAny();System.out.println(opt);


Output:

Optional[43] Optional.empty


Explanation:

Here, in the first case, findFirst() method returns the first element of the stream as an Optional. Next, when the stream is reassigned as an empty stream, findAny() method returns an empty Optional, as was claimed above.


6 - allMatch(), anyMatch() and noneMatch()


allMatch() method is used to check whether all the elements in a stream meet a certain predicate and returns a boolean value “true” if that’s the case, otherwise “false” is returned. If the stream is empty, it returns “true”

anyMatch() method is used to check whether any of the elements in a stream meets a certain predicate. It returns “true” if it does, and “false” otherwise. If the stream is empty, it returns “false.”

noneMatch() method returns “true” if no stream element matches the predicate, and “false” otherwise.

An example to illustrate this is as follows:


Code:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); boolean flag = numStream.allMatch(n -> n1); System.out.println(flag); numStream = Stream.of(43, 65, 1, 98, 63); flag = numStream.anyMatch(n -> n1); System.out.println(flag); numStream = Stream.of(43, 65, 1, 98, 63); flag = numStream.noneMatch(n -> n==1);System.out.println(flag);


Output:

false true false


Explanation:

For the given stream “numStream” containing 1 as an element, allMatch() method returns false as all the elements are not 1, only one of them is; anyMatch() method returns true as at least one of the elements is 1; noneMatch() method returns false as 1 positively exists as an element in the stream.



Lazy Evaluations in Java Streams


Lazy evaluations lead to remarkable optimizations while working with Java Streams in Java 8. These basically involve a delay in the execution of intermediate operations till a terminal operation is encountered.


Lazy evaluations are responsible for preventing unnecessary resources from being wasted on computations till the result is actually needed.


The output stream resulting from intermediate operations is only generated after the terminal operation is executed. Lazy evaluations are functional on all intermediate operations on Java streams.


A very useful application of lazy evaluations comes into the picture while dealing with infinite streams. In the case of infinite streams, a lot of unnecessary processing is prevented with the help of lazy evaluations.

Pipelines in Java Streams

A pipeline, in the case of Java streams, comprises the input stream, zero or multiple intermediate operations lined one after the other, and finally, a terminal operation. **
**

Intermediate operations in Java Streams are lazily executed. This brings pipelining intermediate operations inevitable. With the help of pipelines, which are basically intermediate operations combined in order, lazy execution becomes possible.


Pipelines help keep track of the intermediate operations that need to be executed once a terminal operation is finally encountered.

Conclusion

Let us now summarise what we have studied till now. In this article,


  1. We briefly took a look at what Java streams are.
  2. We then learnt many different methods of creating Java streams in Java 8.
  3. Next up, we studied the two significant kinds of operations (intermediate operations and terminal operations) that can be performed on Java streams.
  4. Then, after this, we saw some examples of both intermediate as well as terminal operations in detail.
  5. In the end, we learnt about lazy evaluations in more detail and finally studied pipelining in Java streams.