Java - Streams
Gain from studying Streams and ParalleStreams :
Something very important when we are going to study some content is to know why we are studying it and what we are going to gain from it. So I will highlight some gains with the use of Streams and ParalleStreams :
The proposal around the Streams API is to make the developer no longer worry about how to program the behavior, leaving the part related to flow control and loop to the API . It is something very similar to what is done with Threads , where the most complex aspects are encapsulated in APIs and the business rules become the sole responsibility of the developer.
This API brings a more functional way of working with our collections. It has several methods, such as filter , map and reduce , which take a functional interface as a parameter, allowing us to take advantage of the new lambda and method reference features .
Using Stream does not impact the existing list, so any filtering operation, for example, will not remove elements from the original list.
Another point concerns processing efficiency. With the proliferation of multicore CPUs , the API took this into account and with the support of the functional paradigm, supports parallelization of operations to process the data, leading to a performance gain in your operations.
What do you use collections for? You don't just store objects in a collection and leave them there. In most cases, you use collections to retrieve items stored in them, and several times you have to perform operations to remove the desired items from that collection.
The designers of the Java API interface have incorporated in the Java 8 update a new abstraction called Stream , which allows you to process data declaratively. Furthermore, streams allow you to take advantage of multi-core architectures without having to program lines of multiprocess code. Sounds good, doesn't it? That's what we're going to explore in this series of posts .
Before getting into the details of what you can do with Stream , let's look at the Person class in Code Snippet 1 and a list of people in Code Snippet 2 . These two items will be very important for understanding most of the examples in this post .
public class Pessoa {
public enum Sexo {
HOMEM, MULHER
}
private Long id;
private String name;
private Long idade;
private Sexo genero;
private String email;
public Pessoa(Long id, String name, Long idade, Sexo genero, String email) {
this.id = id;
this.name = name;
this.idade = idade;
this.genero = genero;
this.email = email;
}
// ... Getter and setter ...
Code snippet 1 : Class Person containing the attributes id, name, age, gender and email. This class will be used in most examples.
public class PessoaFactory {
public static List fabricarPessoas() {
List pessoas = new ArrayList();
pessoas.add(new Pessoa(1L, "Eduardo", 5L, Sexo.HOMEM, "@aztech.com.br"));
pessoas.add(new Pessoa(2L, "Eduarda", 5L, Sexo.MULHER, "@aztech.com.br"));
pessoas.add(new Pessoa(3L, "Bruno", 10L, Sexo.HOMEM, "@aztech.com.br"));
pessoas.add(new Pessoa(4L, "Bruna", 10L, Sexo.MULHER, "@aztech.com.br"));
pessoas.add(new Pessoa(5L, "Fernando", 15L, Sexo.HOMEM, "@aztech.com.br"));
pessoas.add(new Pessoa(6L, "Fernanda", 15L, Sexo.MULHER, "@aztech.com.br"));
pessoas.add(new Pessoa(7L, "Rafael", 20L, Sexo.HOMEM, "@aztech.com.br"));
pessoas.add(new Pessoa(8L, "Rafaela", 20L, Sexo.MULHER, "@aztech.com.br"));
pessoas.add(new Pessoa(9L, "Joao", 25L, Sexo.HOMEM, "@aztech.com.br"));
pessoas.add(new Pessoa(10L, "Juana", 25L, Sexo.MULHER, "@aztech.com.br"));
pessoas.add(new Pessoa(11L, "Mario", 30L, Sexo.HOMEM, "@aztech.com.br"));
pessoas.add(new Pessoa(12L, "Maria", 30L, Sexo.MULHER, "@aztech.com.br"));
pessoas.add(new Pessoa(13L, "Marcio", 50L, Sexo.HOMEM, "@aztech.com.br"));
pessoas.add(new Pessoa(14L, "Marcia", 50L, Sexo.MULHER, "@aztech.com.br"));
pessoas.add(new Pessoa(15L, "Juliano", 55L, Sexo.HOMEM, "@aztech.com.br"));
pessoas.add(new Pessoa(16L, "Juliana", 55L, Sexo.MULHER, "@aztech.com.br"));
pessoas.add(new Pessoa(17L, "Antonio", 65L, Sexo.HOMEM, "@aztech.com.br"));
pessoas.add(new Pessoa(18L, "Antonia", 65L, Sexo.MULHER, "@aztech.com.br"));
pessoas.add(new Pessoa(19L, "Fabricio", 70L, Sexo.HOMEM, "@aztech.com.br"));
pessoas.add(new Pessoa(20L, "Fabricia", 70L, Sexo.MULHER, "@aztech.com.br"));
return pessoas;
}
Code snippet 2 : Class PersonFactory creating a List with 20 people for our examples.
Now let's see an example to get to know the new programming style allowed by Streams . In Code Snippet 3 we see an iteration using a foreach and in Code Snippet 4 we see the same iteration using Stream .
for(Pessoa p :pessoas) {
System.out.println(p.getName());
}
Code snippet 3 : Iteration using foreach.
pessoas.stream()
.forEach(p -> System.out.println(p.getName()));
Code snippet 4 : Iteration using Stream.
Although, in this example, the version that uses aggregate operations ( Streams ) is larger than the one that uses a foreach loop , you will find that streams will be more concise for more complex tasks. Let's see an example in code snippet 5 :
List< Pessoa > pessoas = PessoaFactory.fabricarPessoas();
pessoas.stream()
.filter(p -> p.getGenero().equals(Sexo.HOMEM))
.filter(p -> p.getIdade() <= 10)
.sorted(Comparator.comparing(Pessoa::getId))
.map(Pessoa::getId)
.collect(Collectors.toList())
Code snippet 5 : Query performing a filter by sex and age, followed by sorting by id.
Code snippet 5 shows an example where we first get a stream of the list of people with the stream () method available for the List type . Then several operations ( filter , sorted , map , collect ) are chained together in a process, which can be seen as a query on the data.
Defining better the flow of a stream
A stream represents a sequence of elements and supports different types of operations to perform calculations on these elements:
Flow operations are intermediate or terminal . Intermediate operations return a stream so we can chain together multiple intermediate operations without using a semicolon. Terminal operations are either invalid or return a non- stream result . This chain of flow operations, as seen in the example above, is also known as the operation pipeline .
Most stream operations accept some sort of lambda expression parameter , a functional interface that specifies the exact behavior of the operation. Most of these operations should be non-interfering and stateless. What does that mean?
Non-Interfering Function: This is when it does not modify the underlying data source of the stream. In the example above we saw that no lambda expression adds or removes elements from the People List .
Stateless Function: It is when the operation execution is deterministic. In the example above, we saw that no lambda expression depends on any mutable variables or states of the outer scope that may change during execution.
In Figure 1 we can see a summarized example of a stream flow followed by the description of each step:
Figure 1 : Stream summary of a stream.
- Source – Represents the source of the objects to be manipulated;
- Pipeline – (Zero or more operations) represents the set of intermediate operations that will act on Stream elements . These operations do not execute, they only prepare the frameworks for execution;
- Terminal – represents the final operation that will extract the desired elements. As soon as a final operation is called the intermediate operations ( pipeline ) are executed and then a result is produced.
One important thing to note is that intermediate operations ( pipeline ) are lazy.
An expression, method, or algorithm is lazy if its value is evaluated only when necessary.
An algorithm is anxious if it is evaluated or processed immediately.
Intermediate operations are lazy because they don't start processing the contents of the stream until the endpoint operation starts. Processing streams lazily allows the Java compiler and runtime to optimize processing of streams.
Let's see in Figure 2 an example of a detailed flow. For this example we will use the code from Code snippet 5 .
Figure 2 : Code Snippet Stream Stream 5 .
Note: The stream shown in Figure 2 is for educational purposes only, as in normal operation the stream optimizes the processing of streams. This means that many times the operations may not be executed in the same order or may be merged into a single operation in order to gain performance and perform as little processing as possible.
First, let's get a stream from the list of people by calling the stream () method. Next, we'll apply a series of aggregate operations to the stream : filter (to filter elements according to a particular predicate), sorted (to sort elements according to a comparator) and map (to obtain information). All operations, with the exception of collect , return a Stream , so it's possible to chain them together and form a process, which we can see as querying the source data.
In fact, no task is performed until the collect operation is called.. The latter will begin to address the process of returning a result (which will not be a Stream , in this case it is a List ).
Different types of flows
Flows can be created from various data sources, especially collections. Lists and sets support the new stream () and parallelStream () methods that create a sequential or parallel stream. Parallel streams are capable of operating on multiple threads and will be covered in a future post . We'll focus on sequential streams for now:
We can create a stream stream from a list of objects:
Arrays.asList("a1", "a2", "a3")
.stream()
.forEach(System.out::println);
Flow Type 1 : Creating a flow from a list of objects.
But we don't need to create collections to work with streams, for that we can call the static of method of the Stream class as we see in the next example:
Stream.of("a1", "a2", "a3")
.forEach(System.out::println);
Stream Type 2 : Creating Stream by the of method of the Stream class .
In addition to regular object streams, we can work with primitive data types int, long and double . As you might have guessed IntStream , LongStream and DoubleStream . Let's look at an example:
IntStream.of(1, 4)
.forEach(System.out::println);
Flow Type 3 : Creating Flow from primitive data.
All of these primitive flows work just like regular object flows with the following differences: Primitive flows use specialized lambda expressions ( IntFunction instead of Function or IntPredicate instead of Predicate for example). And primitive streams support additional terminal operations sum () and average ().
Sometimes it is useful to transform a regular object stream into a primitive stream or vice versa. To get the object stream conversion to a primitive stream we can call one of the special mapping methods: mapToInt (), mapToLong () or mapToDouble :
Stream.of("1", "2", "3")
.mapToInt(Integer::parseInt)
.forEach(System.out::println);
Flow Type 4 : Creating Flow primitive from a flow of objects.
Primitive streams can be transformed into object streams via mapToObj ():
IntStream.range(1, 4)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
Flow Type 5 : Creating object flow from a primitive flow.
Checking the lazy flow
Now in Code Snippet 6 we are going to check a code example that shows us the lazy flow of the stream , verifying that the intermediate methods will only be executed when a final method is called. In Console 1 you can see the result of this code.
System.out.println("Criando a stream de fluxo preguiçoso");
Stream< Pessoa > streamPessoa = pessoas.stream()
.filter(p -> {
System.out.println("passando pelo filtro e verificando pessoa");
return p.getIdade() <= 10;
})
.sorted(Comparator.comparing(Pessoa::getName));
System.out.println("Terminou a criação do fluxo preguiçoso");
streamPessoa.forEach(p -> System.out.println(p.getName()));
Code snippet 6 : Query showing lazy stream of stream.
Criando a stream de fluxo preguiçoso
Terminou a criação do fluxo preguiçoso
passando pelo filtro e verificando pessoa
passando pelo filtro e verificando pessoa
...
passando pelo filtro e verificando pessoa
passando pelo filtro e verificando pessoa
Bruna
Bruno
Eduarda
Eduardo
Console 1 : Result of Code Snippet 6.
We can see that the first message to be printed is "Creating lazy stream stream" followed by "Finished creating lazy stream" and only after the forEach function is called is the message "passing through filter and checking person" is printed. In this way we can see that all intermediate operations are only called when a final function is executed.
Checking for code optimization.
Now in Code snippet 7 we will check a code example that shows us the optimization performed by the stream . This code calculates two even powers from a list of numbers.
List< Integer > numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
List< Integer > twoEvenSquares =
numbers.stream()
.filter(n -> {
System.out.println("filtering " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("mapping " + n);
return n * n;
})
.limit(2)
.collect(Collectors.toList());
Code snippet 7 : Query showing lazy stream of stream.
Perhaps the result shown in Console 2 is not quite what the reader expected.
filtering 1
filtering 2
mapping 2
filtering 3
filtering 4
mapping 4
Console 2 : Result of Code Snippet 7.
This is because in limit (2) the circuit is shortened, so to return a result it is only necessary to process part of the stream , not all of it. A similar situation arises when evaluating a long Boolean expression chained with the and operator : as soon as the expression returns the value false , it is possible to deduce that the entire expression is false without evaluating it completely. In this case, limit returns a stream of size 2.
In addition, the filter and map operations were merged into a single action.
Intermediate Operations/Pipeline.
In this topic we will look at some of the intermediate methods. All the examples shown in this topic are based on Code snippet 1 where we have the person class and on Code snippet 2 we have a List of people.
- FILTER
It is the method defined in the Stream interface that performs a 'filter' on the elements of a collection. This method takes a predicate ( java.util.function.Predicate ) and returns a stream including all elements that match the indicated predicate.
pessoas.stream()
.filter(p -> p.getName().startsWith("Eduard"))
.forEach(p -> System.out.println(p.getName()));
Code snippet 8 : Performs a query bringing all the people starting with "Eduard".
Eduardo
Eduarda
Console 3 : Result of Code Snippet 8 .
- SORTED
It is the method defined in the Stream interface that performs sorting on the elements of a collection. This method takes a comparator (java.util.Comparator) and returns a stream corresponding to the result of the comparator .
pessoas.stream()
.filter(p -> p.getGenero().equals(Sexo.HOMEM))
.sorted(Comparator.comparing(Pessoa::getName))
.forEach(p -> System.out.println(p.getName()));
Code snippet 9 : Performs a query bringing all men sorted by name.
Antonio
Bruno
Eduardo
Fabricio
Fernando
Joao
Juliano
Marcio
Mario
Rafael
Console 4 : Result of Code Snippet 9 .
- MAP It is the method defined in the Stream
interface that converts each received element into another object, according to the passed function. This method takes a function (java.util.function.Function) and returns the result of the function generating a new stream .
pessoas.stream()
.filter(p -> p.getGenero().equals(Sexo.HOMEM))
.map(Pessoa::getName)
.forEach(System.out::println);
Code snippet 10 : Performs a query bringing all the men, then the map method returns a String stream containing the names of each person.
Eduardo
Bruno
Fernando
Rafael
Joao
Mario
Marcio
Juliano
Antonio
Fabricio
Console 5 : Result of Code Snippet 10 .
- DISTINCT
Returns a stream with unique elements (depending on the object 's equals implementation).
Pessoa testeDistinct = new Pessoa(1L, "Eduardo", 5L, Sexo.HOMEM, "@aztech.com.br");
List pessoas = new ArrayList();
pessoas.add(testeDistinct);
pessoas.add(testeDistinct);
pessoas.add(testeDistinct);
pessoas.stream()
.distinct()
.forEach(p -> System.out.println(p.getName()));
Code snippet 11 : Inserts the same person three times into the List and uses the distinct method available for stream to remove duplicate elements.
Eduardo
Console 6 : Result of Code Snippet 11 .
- LIMIT
Returns a stream whose maximum length is the int parameter passed.
pessoas.stream()
.limit(4)
.forEach(p -> System.out.println(p.getName()));
Code snippet 12 : Returns only 4 elements from the People List.
Eduardo
Eduarda
Bruno
Bruna
Console 7 : Result of Code Snippet 12 .
- SKIP
Returns a stream in which the first n numbers were discarded, where n is an int passed by parameter.
pessoas.stream()
.skip(16)
.forEach(p -> System.out.println(p.getName()));
Code snippet 13 :
Antonio
Antonia
Fabricio
Fabricia
https :// www . baeldung _ com / java - 8 - streams - introductionConsole 8 : Result of Code Snippet 13 .
Final/Terminal Operations
- MATCH
It is defined in the Stream interface that checks if a certain condition is met. Its methods are allMatch , anyMatch , and noneMatch where they all take a predicate (java.util.function.Predicate) and return a boolean.
- allMatch : Checks if all items have the true Predicate .
- anyMatch : Checks if any item has the true Predicate .
- noneMatch : Checks if no items have the true Predicate .
pessoas.stream()
.allMatch(p -> p.getGenero().equals(Sexo.HOMEM));//return false
//Existe somente homens na lista.
pessoas.stream()
.anyMatch(p -> p.getGenero().equals(Sexo.HOMEM));//return true
//Existe homem na lista
pessoas.stream()
.noneMatch(p -> p.getGenero().equals(Sexo.HOMEM));//return false
//Não existe homem na lista
Code snippet 14 :
-COUNT
Is the method defined in the Stream interface that returns the number of elements in the stream . This method returns a long and takes no parameters.
long quantidade;
quantidade = pessoas.stream()
.filter(pessoa -> pessoa.getIdade() > 50)
.count();
System.out.println(quantidade);
Code snippet 15 : Count method returning how many people are over 50 years old.
6
Console 9 : Result of Code Snippet 15 .
- COLLECT
It's a method defined in the Stream interface , and probably only this method would be enough to write another post lol.
Briefly Collect can transform the Stream stream into list , maps , set and perform other operations.
Several predefined implementations for Collect can be found in the Collectors class ( java.util.stream.Collectors ), among them are:
- Collectors.toList
- Collectors.toMap
- Collectors.toSet
List novaLista = pessoas.stream()
.filter(p -> p.getGenero().equals(Sexo.HOMEM))
.filter(p -> p.getIdade() <= 10)
.collect(Collectors.toList());
novaLista.forEach(pessoa -> System.out.println(pessoa.getName()));
Code snippet 16 : .
Eduardo
Bruno
Comments
Post a Comment