JavaRush /Java Blog /Random EN /Coffee break #177. Detailed guide to Java Stream in Java ...

Coffee break #177. Detailed guide to Java Stream in Java 8

Published in the Random EN group
Source: Hackernoon This post provides a detailed guide to working with Java Stream along with code examples and explanations. Coffee break #177.  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 by themselves, but can be used to take information from other data structures by ordering and piping to produce the final result. Note: It is important not to confuse Stream and Thread, because in Russian both terms are often mentioned in the same translation “stream”. Stream denotes an object for performing operations (most often data transfer or accumulation), while Thread (literal translation - thread) denotes an object that allows you to execute certain program code in parallel with other code branches. Since Stream is not a separate data structure, it never changes the data source. Java threads have the following features:
  1. Java Stream can be used with 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 collections of data such as collections and arrays in Java.

  3. Java Stream does not require any change in the structure of the input data.

  4. Java Stream does not change the source. Instead, it generates output using the 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 the Java Stream.

In the next section, we'll explore the various ways Java 8 uses to create a Java Stream as needed.

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 thread for use in later code steps. Using the Stream.empty() method will generate an empty stream containing no values. This empty stream can come in handy if we want to skip the 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, it is enough to 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 on the output .

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

We can also use the Stream Builder to create a stream using the builder design pattern. It is intended for step-by-step construction of objects. Let's see how we can instantiate a stream using the 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 fast enough thanks to the Stream.Builder instance named numBuilder , which 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 an easy way to create a thread 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 , as we did in the previous method using Stream.Builder . Here we have directly created a stream using Stream.of() with predefined values ​​of [1, 2 and 3] .

4. Creating 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 is 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 named arr, which is an integer array.

5. Combining 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 streams to create a single stream. Both streams are merged in order. This means that the first thread comes first, followed by the second thread, and so on. An example of such a 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, one after the other, the elements of the first stream numStream1 and the second stream numStream2 .

Operation types with Java Stream

As already mentioned, two types of operations can be performed with Java Stream in Java 8: intermediate (intermediate) and terminal (terminal). Let's consider each of them in more detail.

intermediate operations

Intermediate operations generate an output stream and are executed only when they encounter a terminal operation. This means that intermediate operations are performed lazily, pipelined, and can only be completed by a terminal operation. You'll learn about lazy evaluation and pipelining in a bit. 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. Since 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.

Java Stream Operation Examples

intermediate operations

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

filter()

This method is used to filter elements from a stream that match a certain predicate in Java. These filtered elements 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 the even elements (divisible by 2) are filtered using the filter() method and stored in the integer list numStream , whose contents 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 the mapped functions on the elements of the original input stream. Perhaps the new stream has a different data type. An 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 . As you can see from the output, each of the elements in the stream has been successfully doubled.

distinct()

This method is used to extract 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 on numStream . It extracts all 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 originally 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 the 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 matching is printed sequentially ahead of time.

sorted()

The sorted() method is used to sort the elements of a stream. By default, it sorts the 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 (because no specific order is specified). You can see that the items printed in the output are sorted 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 loop through all the elements of a stream and execute a function on each element one by one. This acts as an alternative to loop statements like 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 Java Stream is as follows:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); System.out.println(numStream.count());
Conclusion:
5
Explanation: Because numStream contains 5 integer elements, using the count() method on it will output 5.

collect()

The collect() method is used to perform mutable reductions on stream elements. It can be used to remove content from a stream after processing is complete. 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 . A list of odd numbers is printed at the end.

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 applying the max() method , we again added elements to the numStream . 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 "smallest" integer).

findAny() and findFirst()

findAny() returns any element of the stream as an 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 an Optional . As with the findAny() method , the findFirst() method also returns an empty Optional 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 an Optional . Then, when the stream is reassigned as an empty stream, the findAny() method returns an empty Optional .

allMatch() , anyMatch() and noneMatch()

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

Lazy Evaluations (lazy evaluation) in Java Stream

Lazy evaluation leads to optimizations when working with Java Streams in Java 8. They are mainly related to delaying the execution of 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 obtained as a result of intermediate operations is generated only after the execution of the terminal operation. Lazy evaluation works with all intermediate operations in Java streams. A very useful use of lazy evaluation is when dealing with infinite streams. In this case, a lot of unnecessary processing is prevented.

Pipelines in Java Stream

A pipeline in a Java Stream includes 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 lazy. This makes pipeline 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 intermediate operations that need to be performed after the terminal operation is finally encountered.

Conclusion

Let's now summarize what we have learned today. In this article:
  1. We briefly looked at what Java streams (Java Stream) are.
  2. We then learned many different methods for creating Java threads in Java 8.
  3. We have learned about the 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. As a result, we learned more about lazy evaluation and finally learned about pipelining in Java threads.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION