JavaRush /Java Blog /Random EN /Coffee break #177. A Detailed Guide to Java Stream in Jav...

Coffee break #177. A Detailed Guide to Java Stream in Java 8

Published in the Random EN group
Source: Hackernoon This post provides a detailed tutorial on working with Java Stream along with code examples and explanations. Coffee break #177.  A Detailed Guide to Java Stream in Java 8 - 1

Introduction to Java Threads in Java 8

Java Streams, introduced as part of Java 8, are used to work with collections of data. They are not a data structure themselves, but can be used to input information from other data structures by ordering and pipelining to produce a final result. Note: It is important not to confuse Stream and Thread, since in Russian both terms are often referred to in the same translation “flow”. Stream denotes an object for performing operations (most often transferring or storing data), while Thread (literal translation - thread) denotes an object that allows certain program code to be executed in parallel with other code branches. Because Stream is not a separate data structure, it never changes the data source. Java streams have the following features:
  1. Java Stream can be used using the “java.util.stream” package. It can be imported into a script using the code:

    import java.util.stream.* ;

    Using this code, we can also easily implement several built-in functions in Java Stream.

  2. Java Stream can accept input from data collections such as collections and arrays in Java.

  3. Java Stream does not require changing the input data structure.

  4. Java Stream does not change the source. Instead, it generates output using appropriate pipeline methods.

  5. Java Streams are subject to intermediate and terminal operations, which we will discuss in the following sections.

  6. In Java Stream, intermediate operations are pipelined and occur in a lazy evaluation format. They end with terminal functions. This forms the basic format for using Java Stream.

In the next section, we will look at the various methods used in Java 8 to create a Java Stream as and when required.

Creating a Java Stream in Java 8

Java threads can be created in several ways:

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

You can create an empty stream for use later in your code. If you use the Stream.empty() method , an empty stream will be generated, containing no values. This empty stream can come in handy if we want to skip a null pointer exception at runtime. To do this you can use the following command:
Stream<String> str = Stream.empty();
The above statement will generate an empty stream named str without any elements inside it. To verify this, just check the number or size of the stream using the term str.count() . For example,
System.out.println(str.count());
As a result, we get 0 at the output .

2. Create a stream using the Stream.builder() method with a Stream.Builder instance

We can also use Stream Builder to create a stream using the builder design pattern. It is designed for step-by-step construction of objects. Let's see how we can instantiate 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 code, you can create a stream named numStream containing int elements . Everything is done quite quickly thanks to the Stream.Builder instance called numBuilder that is created first.

3. Create a stream with the specified values ​​using the Stream.of() method

Another way to create a stream involves using the Stream.of() method . This is a simple way to create a stream with given values. It declares and also initializes the thread. An example of using the Stream.of() method to create a stream:
Stream<Integer> numStream = Stream.of(1, 2, 3);
This code will create a stream containing int elements , just like we did in the previous method using Stream.Builder . Here we have directly created a stream using Stream.of() with predefined values ​​[1, 2 and 3] .

4. Create a stream from an existing array using the Arrays.stream() method

Another common method for creating a thread involves using arrays in Java. The stream here is created from an existing array using the Arrays.stream() method . All array elements are converted to stream elements. Here's a good example:
Integer[] arr = {1, 2, 3, 4, 5};

Stream<Integer> numStream = Arrays.stream(arr);
This code will generate a numStream containing the contents of an array called arr, which is an integer array.

5. Merging two existing streams using the Stream.concat() method

Another method that can be used to create a stream is the Stream.concat() method . It is used to combine two threads to create a single thread. Both streams are combined in order. This means that the first thread comes first, followed by the second thread, and so on. An example of such concatenation looks like this:
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 will create a final stream named combinedStream containing elements of the first stream numStream1 and the second stream numStream2 one by one .

Types of operations with Java Stream

As already mentioned, you can perform two types of operations with Java Stream in Java 8: intermediate and terminal. Let's look at each of them in more detail.

Intermediate operations

Intermediate operations generate an output stream and are executed only when encountering a terminal operation. This means that intermediate operations are executed lazily, pipelined, and can only be completed by a terminal operation. You'll learn about lazy evaluation and pipelining a little later. Examples of intermediate operations are the following methods: filter() , map() , different() , peek() , sorted() and some others.

Terminal Operations

Terminal operations complete the execution of intermediate operations and also return the final results of the output stream. Because terminal operations signal the end of lazy execution and pipelining, this thread cannot be used again after it has undergone a terminal operation. Examples of terminal operations are the following methods: forEach() , collect() , count() , reduce() and so on.

Examples of operations with Java Stream

Intermediate operations

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

filter()

This method is used to filter elements from a stream that match a specific predicate in Java. These filtered items then make up a new stream. Let's take a look at an example to better understand better.
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);
Conclusion:
[98]
Explanation: In this example, you can see that even elements (divisible by 2) are filtered using the filter() method and stored in an integer list numStream , the contents of which are printed later. Since 98 is the only even integer in the stream, it is printed in the output.

map()

This method is used to create a new stream by executing mapped functions on elements of the original input stream. Perhaps the new stream has a different data type. The example looks like this:
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);
Conclusion:
[86, 130, 2, 196, 126]
Explanation: Here we see that the map() method is used to simply double each element of the numStream stream . As you can see from the output, each of the elements in the stream has been successfully doubled.

distinct()

This method is used to retrieve only individual elements in a stream by filtering out duplicates. An example of the same looks like this:
Stream<Integer> numStream = Stream.of(43,65,1,98,63,63,1); List<Integer> numList = numStream.distinct() .collect(Collectors.toList()); System.out.println(numList);
Conclusion:
[43, 65, 1, 98, 63]
Explanation: In this case, the different() method is used for numStream . It retrieves all the individual elements in numList by removing duplicates from the stream. As you can see from the output, there are no duplicates, unlike the input stream, which initially had two duplicates (63 and 1).

peek()

This method is used to track intermediate changes before executing a terminal operation. This means that peek() can be used to perform an operation on each element of a stream to create a stream on which further intermediate operations can be performed.
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);
Conclusion:
Mapped: 430 Mapped: 650 Mapped: 10 Mapped: 980 Mapped: 630 [430, 650, 10, 980, 630]
Explanation: Here the peek() method is used to generate intermediate results as the map() method is applied to the elements of the stream. Here we can notice that even before using the collect() terminal operation to print the final contents of the list in the print statement , the result for each stream element mapping is printed sequentially in advance.

sorted()

The sorted() method is used to sort the elements of a stream. By default, it sorts elements in ascending order. You can also specify a specific sort order as a parameter. An example implementation of this method looks like this:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.sorted().forEach(n -> System.out.println(n));
Conclusion:
1 43 ​​63 65 98
Explanation: Here, the sorted() method is used to sort the elements of the stream in ascending order by default (since no specific order is specified). You can see that the items printed in the output are arranged in ascending order.

Terminal Operations

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

forEach()

The forEach() method is used to iterate through all the elements of a stream and execute the function on each element one by one. This acts as an alternative to loop statements such as for , while and others. Example:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.forEach(n -> System.out.println(n));
Conclusion:
43 65 1 98 63
Explanation: Here the forEach() method is used to print each element of the stream one by one.

count()

The count() method is used to retrieve the total number of elements present in the stream. It is similar to the size() method , which is often used to determine the total number of elements in a collection. An example of using the count() method with a Java Stream is as follows:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); System.out.println(numStream.count());
Conclusion:
5
Explanation: Since numStream contains 5 integer elements, using the count() method on it will output 5.

collect()

The collect() method is used to perform mutable reductions of stream elements. It can be used to remove content from a stream after processing has completed. It uses the Collector class to perform reductions .
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);
Conclusion:
[43, 65, 1, 63]
Explanation: In this example, all odd elements in the stream are filtered and collected/reduced into a list named odd . At the end, a list of odd ones is printed.

min() and max()

The min() method , as the name suggests, can be used on a stream to find the minimum element in it. Similarly, the max() method can be used to find the maximum element in a stream. Let's try to understand how they can be used with an example:
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);
Conclusion:
Smallest element: 1 Largest element: 98
Explanation: In this example, we printed the smallest element in the numStream using the min() method and the largest element using the max() method . Note that here, before using the max() method , we have added elements to the numStream again . This is because min() is a terminal operation and destroys the contents of the original stream, returning only the final result (which in this case was the integer “smallest”).

findAny() and findFirst()

findAny() returns any element of the stream as Optional . If the stream is empty, it will also return an Optional value , which will be empty. findFirst() returns the first element of the stream as Optional . As with the findAny() method , the findFirst() method also returns an empty Optional parameter if the corresponding stream is empty. Let's take a look at the following example based on these methods:
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);
Conclusion:
Optional[43] Optional.empty
Explanation: Here, in the first case, the findFirst() method returns the first element of the stream as Optional . Then, when the thread is reassigned as an empty thread, the findAny() method returns an empty Optional .

allMatch() , anyMatch() and noneMatch()

The allMatch() method is used to check whether all elements in a stream match a certain predicate and returns the boolean value true if they do, otherwise returns false . If the stream is empty, it returns true . The anyMatch() method is used to check whether any of the elements in a stream matches a certain predicate. It returns true if so, false otherwise. If the stream is empty, it returns false . The noneMatch() method returns true if no element in the stream matches the predicate, and false otherwise. An example to illustrate this looks like this:
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);
Conclusion:
false true false
Explanation: For a stream numStream containing 1 as an element, the allMatch() method returns false because all elements are not 1, but only one of them is. The anyMatch() method returns true because at least one of the elements is 1. The noneMatch() method returns false because 1 actually exists as an element in this stream.

Lazy Evaluations in Java Stream

Lazy evaluation leads to optimizations when working with Java Streams in Java 8. They mainly involve delaying intermediate operations until a terminal operation is encountered. Lazy evaluation is responsible for preventing unnecessary waste of resources on calculations until the result is actually needed. The output stream resulting from intermediate operations is generated only after the terminal operation has completed. Lazy evaluation works with all intermediate operations in Java streams. A very useful use of lazy evaluation occurs when working with infinite streams. This way, a lot of unnecessary processing is prevented.

Pipelines in Java Stream

A pipeline in a Java Stream consists of an input stream, zero or more intermediate operations lined up one after the other, and finally a terminal operation. Intermediate operations in Java Streams are performed lazily. This makes pipelined intermediate operations inevitable. With pipelines, which are basically intermediate operations combined in order, lazy execution becomes possible. Pipelines help keep track of intermediate operations that need to be performed after a terminal operation is finally encountered.

Conclusion

Let us now summarize what we have learned today. In this article:
  1. We took a quick look at what Java Streams are.
  2. We then learned many different techniques for creating Java threads in Java 8.
  3. We learned two main types of operations (intermediate operations and terminal operations) that can be performed on Java streams.
  4. We then looked in detail at several examples of both intermediate and terminal operations.
  5. We ended up learning more about lazy evaluation and finally learning about pipelining in Java threads.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION