API de Transmissão
Existem várias maneiras de processar dados em Java - loops, métodos e diferentes algoritmos. No entanto, no Java 8, foi introduzida uma ferramenta muito poderosa - a API Stream.
Em termos simples, a API de Stream é uma maneira de trabalhar de forma rápida e fácil com um fluxo de informações. No nosso caso, esse fluxo de informações é representado por coleções. A API de Stream possui alguns conceitos. Aqui estão os principais.
Conceitos Principais
- Stream: Representa uma sequência de elementos de dados que podem ser processados.
- Operações Intermediárias: Operações que criam um novo stream após a sua execução. Exemplos:
filter
,map
,distinct
,sorted
. - Operações Terminais: Operações que completam o processamento do stream e retornam um resultado. Exemplos:
collect
,forEach
,count
,reduce
. - Streams Paralelos: Permitem o processamento paralelo de dados. Os métodos
parallel()
eparallelStream()
são utilizados para criar streams paralelos.
Chega de falar sobre teoria, vamos começar a programar!
A declaração de um stream é feita utilizando um método na coleção que desejamos transformar em stream:
main.java
Com o método stream()
, obtivemos um fluxo de strings. Porém, para começar a trabalhar com o fluxo, precisamos entender o que são expressões lambda, pois os métodos de fluxo trabalham principalmente com elas.
Expressões Lambda
Expressões lambda foram introduzidas no Java 8 e representam uma forma simplificada de criar funções anônimas em Java. Ainda não abordamos funções anônimas anteriormente, pois não eram extremamente necessárias, mas agora vamos nos familiarizar com elas por meio das expressões lambda.
Sintaxe da Expressão Lambda:
A sintaxe geral para expressões lambda em Java é a seguinte:
example.java
- Parâmetros: Esta é uma lista de parâmetros que pode estar vazia ou conter um ou mais parâmetros.
- Seta: Representada pelo símbolo
->
, que separa os parâmetros do corpo da expressão lambda. - Expressão ou Declarações: Este é o corpo da função, contendo uma expressão ou um bloco de instruções.
Aqui está um exemplo de uma expressão lambda representando uma função simples que soma dois números:
example.java
Vamos examinar mais de perto o que está acontecendo no código acima e como usamos expressões lambda:
main.java
Explicação:
- Criada uma interface funcional
MyMathOperation
com um único método abstratooperate
. - Utilizada uma expressão lambda para implementar este método, realizando a soma de dois números.
- Impresso o resultado da adição.
Entendo que possa ser desafiador compreender o que está acontecendo neste código por enquanto, mas vamos voltar para a API Stream, onde expressões lambda são frequentemente utilizadas, e tentar entender como usá-las na prática.
Como você deve se lembrar, anteriormente, nós criamos um fluxo de strings a partir de uma lista de strings. Agora, vamos utilizar métodos de stream para transformar cada string deste fluxo em maiúsculas:
main.java
No código acima, utilizamos uma expressão lambda e dois métodos: map()
e toList()
. Se está claro o que o método toList()
faz, o método map()
altera cada elemento no fluxo de acordo com a expressão lambda fornecida.
Vamos examinar mais de perto como a expressão lambda funciona aqui:
O método map()
aplica o método toUpperCase()
em cada elemento do fluxo. Nós definimos o elemento deste fluxo como e
e, utilizando a expressão lambda, instruímos o programa a aplicar este método a cada elemento.
Mas isso não é o fim ainda, pois aplicamos uma operação intermediária. Isso significa que as operações no fluxo ainda não foram concluídas. Para completar o trabalho no fluxo, precisamos aplicar uma operação terminal, que finalizará as operações no fluxo e retornará um valor específico. Por exemplo, podemos usar o método toList()
, e o fluxo modificado será convertido em uma lista.
Por exemplo:
main.java
Nota
Observe que após usar a operação terminal, não é mais possível utilizar métodos de stream. No nosso caso, após a operação terminal
toList()
, nosso stream foi convertido em uma lista, logo não podemos usar métodos de stream na lista.
Vamos examinar mais de perto as possíveis operações intermediárias no stream.
Operações intermediárias
- O método
map()
- você já está familiarizado com este método; ele realiza operações especificadas pela expressão lambda em cada elemento do fluxo.
Por exemplo, vamos utilizar o método substring()
em cada elemento no fluxo de strings:
main.java
- O método
filter()
recebe uma expressão lambda com uma condição com base na qual o fluxo será filtrado. Em outras palavras, todos os elementos que atendem à condição irão permanecer no fluxo, e elementos que não atendem à condição serão removidos do fluxo. Vamos modificar o fluxo para manter apenas os elementos cujo comprimento é maior que 5:
main.java
Usando o método filter()
, removemos a string "with" do fluxo porque esta palavra tem menos de 5 caracteres.
Você também pode usar operações intermediárias várias vezes seguidas.
Por exemplo, podemos simplificar um pouco o código acima:
main.java
Ao encadear múltiplos métodos de stream juntos, é recomendável colocar cada método em uma nova linha para melhorar significativamente a legibilidade do código.
- O método
flatMap()
transforma cada elemento de um fluxo em um novo fluxo e combina os resultados em um único fluxo. Em outras palavras, com esse método, podemos dividir o fluxo em fluxos, e então eles serão mesclados em um único fluxo. Por exemplo, temos uma lista de strings onde cada string pode conter mais de uma palavra, como uma lista de nomes e sobrenomes. E precisamos capitalizar a primeira letra de cada uma dessas palavras:
main.java
No código acima, escrevemos um método privado separado que capitaliza a primeira letra de uma palavra e utilizamos este método no método map()
juntamente com uma expressão lambda.
Note que ao usar o método flatMap
, dividimos cada elemento do fluxo em diferentes fluxos usando o método Arrays.stream(e.split(" "))
. Como o método split()
retorna um array, precisamos usar o método Arrays.stream()
para dividir este array em fluxos.
Depois, todos esses fluxos são fundidos em um único fluxo, após o qual usamos o método que escrevemos. E agora temos todos os nomes e sobrenomes dos usuários com a primeira letra maiúscula.
Sabe o que seria legal?
Se colocássemos esses nomes e sobrenomes em um HashMap
, onde a chave é o sobrenome e o valor é o nome.
Vamos implementar isso no código:
main.java
Com um loop simples, armazenamos o nome e o sobrenome em variáveis e depois no mapa. Observe como o loop funciona. Incrementamos a variável i
em 2 a cada iteração porque precisamos pular o sobrenome depois de já tê-lo registrado.
Nota
Este método de filtragem de dados é bastante arriscado porque os dados podem ser registrados na ordem errada, mas no nosso caso, isso não tem um papel significativo.
- O método
distinct()
remove duplicatas do fluxo. Em geral, isso pode ser útil se você precisar de elementos únicos no fluxo ou se quiser eliminar rapidamente duplicatas de uma lista. Você pode facilmente conseguir isso com a seguinte construção:
- O método
sorted
ordena todos os elementos do fluxo em ordem natural, do menor para o maior número ou em ordem alfabética. Isso também pode ser útil se você precisar de um fluxo ordenado ou se precisar ordenar rapidamente uma lista. - O método
skip(n)
ignora os primeirosn
elementos do fluxo. Isso pode ser útil ao trabalhar com arquivos de texto, onde as primeiras n linhas podem ser, por exemplo, metadados ou uma descrição do arquivo. Vale também mencionar o métodolimit(n)
, que geralmente limita o número de elementos no fluxo. Mesmo que criemos um fluxo com 1000 elementos e depois usemoslimit(200)
, o fluxo conterá apenas os primeiros 200 elementos.
main.java
Estes são os principais métodos intermediários que você precisará usar. Você pode explorar o restante dos métodos consultando a link para a documentação oficial do Java. Vamos prosseguir com os métodos terminais.
Métodos Terminais
- O método terminal com o qual você já está familiarizado é
toList()
. Ele converte o fluxo em uma lista e a retorna. Em outras palavras, podemos atribuir diretamente este fluxo com métodos a uma lista. Este método foi introduzido no Java 17 e serve como substituto para a construção mais complexacollect(Collectors.toList())
. - O método
collect()
também converte o fluxo em uma estrutura de dados específica. Ele utiliza, como parâmetro, um método da interfaceCollectors
. Esta interface possui métodos comotoList()
,toSet()
etoCollection()
. Por exemplo:
main.java
O método forEach()
recebe uma expressão lambda e realiza uma ação específica para cada elemento no stream.
Por exemplo:
main.java
A diferença entre este método e o método map
é que este método é terminal, e após utilizá-lo, não é possível chamar outros métodos.
Estes são todos os métodos básicos para trabalhar com streams. É um tópico complexo e talvez você não o compreenda imediatamente. Contudo, é um assunto que se domina com a prática. Nos próximos capítulos práticos sobre streams, você terá várias oportunidades para trabalhar com eles, já que é uma forma muito conveniente e prática de manipular listas e arrays de dados!
Tudo estava claro?
Conteúdo do Curso
Java Data Structures
2. Estruturas de Dados Adicionais
Java Data Structures
API de Transmissão
Existem várias maneiras de processar dados em Java - loops, métodos e diferentes algoritmos. No entanto, no Java 8, foi introduzida uma ferramenta muito poderosa - a API Stream.
Em termos simples, a API de Stream é uma maneira de trabalhar de forma rápida e fácil com um fluxo de informações. No nosso caso, esse fluxo de informações é representado por coleções. A API de Stream possui alguns conceitos. Aqui estão os principais.
Conceitos Principais
- Stream: Representa uma sequência de elementos de dados que podem ser processados.
- Operações Intermediárias: Operações que criam um novo stream após a sua execução. Exemplos:
filter
,map
,distinct
,sorted
. - Operações Terminais: Operações que completam o processamento do stream e retornam um resultado. Exemplos:
collect
,forEach
,count
,reduce
. - Streams Paralelos: Permitem o processamento paralelo de dados. Os métodos
parallel()
eparallelStream()
são utilizados para criar streams paralelos.
Chega de falar sobre teoria, vamos começar a programar!
A declaração de um stream é feita utilizando um método na coleção que desejamos transformar em stream:
main.java
Com o método stream()
, obtivemos um fluxo de strings. Porém, para começar a trabalhar com o fluxo, precisamos entender o que são expressões lambda, pois os métodos de fluxo trabalham principalmente com elas.
Expressões Lambda
Expressões lambda foram introduzidas no Java 8 e representam uma forma simplificada de criar funções anônimas em Java. Ainda não abordamos funções anônimas anteriormente, pois não eram extremamente necessárias, mas agora vamos nos familiarizar com elas por meio das expressões lambda.
Sintaxe da Expressão Lambda:
A sintaxe geral para expressões lambda em Java é a seguinte:
example.java
- Parâmetros: Esta é uma lista de parâmetros que pode estar vazia ou conter um ou mais parâmetros.
- Seta: Representada pelo símbolo
->
, que separa os parâmetros do corpo da expressão lambda. - Expressão ou Declarações: Este é o corpo da função, contendo uma expressão ou um bloco de instruções.
Aqui está um exemplo de uma expressão lambda representando uma função simples que soma dois números:
example.java
Vamos examinar mais de perto o que está acontecendo no código acima e como usamos expressões lambda:
main.java
Explicação:
- Criada uma interface funcional
MyMathOperation
com um único método abstratooperate
. - Utilizada uma expressão lambda para implementar este método, realizando a soma de dois números.
- Impresso o resultado da adição.
Entendo que possa ser desafiador compreender o que está acontecendo neste código por enquanto, mas vamos voltar para a API Stream, onde expressões lambda são frequentemente utilizadas, e tentar entender como usá-las na prática.
Como você deve se lembrar, anteriormente, nós criamos um fluxo de strings a partir de uma lista de strings. Agora, vamos utilizar métodos de stream para transformar cada string deste fluxo em maiúsculas:
main.java
No código acima, utilizamos uma expressão lambda e dois métodos: map()
e toList()
. Se está claro o que o método toList()
faz, o método map()
altera cada elemento no fluxo de acordo com a expressão lambda fornecida.
Vamos examinar mais de perto como a expressão lambda funciona aqui:
O método map()
aplica o método toUpperCase()
em cada elemento do fluxo. Nós definimos o elemento deste fluxo como e
e, utilizando a expressão lambda, instruímos o programa a aplicar este método a cada elemento.
Mas isso não é o fim ainda, pois aplicamos uma operação intermediária. Isso significa que as operações no fluxo ainda não foram concluídas. Para completar o trabalho no fluxo, precisamos aplicar uma operação terminal, que finalizará as operações no fluxo e retornará um valor específico. Por exemplo, podemos usar o método toList()
, e o fluxo modificado será convertido em uma lista.
Por exemplo:
main.java
Nota
Observe que após usar a operação terminal, não é mais possível utilizar métodos de stream. No nosso caso, após a operação terminal
toList()
, nosso stream foi convertido em uma lista, logo não podemos usar métodos de stream na lista.
Vamos examinar mais de perto as possíveis operações intermediárias no stream.
Operações intermediárias
- O método
map()
- você já está familiarizado com este método; ele realiza operações especificadas pela expressão lambda em cada elemento do fluxo.
Por exemplo, vamos utilizar o método substring()
em cada elemento no fluxo de strings:
main.java
- O método
filter()
recebe uma expressão lambda com uma condição com base na qual o fluxo será filtrado. Em outras palavras, todos os elementos que atendem à condição irão permanecer no fluxo, e elementos que não atendem à condição serão removidos do fluxo. Vamos modificar o fluxo para manter apenas os elementos cujo comprimento é maior que 5:
main.java
Usando o método filter()
, removemos a string "with" do fluxo porque esta palavra tem menos de 5 caracteres.
Você também pode usar operações intermediárias várias vezes seguidas.
Por exemplo, podemos simplificar um pouco o código acima:
main.java
Ao encadear múltiplos métodos de stream juntos, é recomendável colocar cada método em uma nova linha para melhorar significativamente a legibilidade do código.
- O método
flatMap()
transforma cada elemento de um fluxo em um novo fluxo e combina os resultados em um único fluxo. Em outras palavras, com esse método, podemos dividir o fluxo em fluxos, e então eles serão mesclados em um único fluxo. Por exemplo, temos uma lista de strings onde cada string pode conter mais de uma palavra, como uma lista de nomes e sobrenomes. E precisamos capitalizar a primeira letra de cada uma dessas palavras:
main.java
No código acima, escrevemos um método privado separado que capitaliza a primeira letra de uma palavra e utilizamos este método no método map()
juntamente com uma expressão lambda.
Note que ao usar o método flatMap
, dividimos cada elemento do fluxo em diferentes fluxos usando o método Arrays.stream(e.split(" "))
. Como o método split()
retorna um array, precisamos usar o método Arrays.stream()
para dividir este array em fluxos.
Depois, todos esses fluxos são fundidos em um único fluxo, após o qual usamos o método que escrevemos. E agora temos todos os nomes e sobrenomes dos usuários com a primeira letra maiúscula.
Sabe o que seria legal?
Se colocássemos esses nomes e sobrenomes em um HashMap
, onde a chave é o sobrenome e o valor é o nome.
Vamos implementar isso no código:
main.java
Com um loop simples, armazenamos o nome e o sobrenome em variáveis e depois no mapa. Observe como o loop funciona. Incrementamos a variável i
em 2 a cada iteração porque precisamos pular o sobrenome depois de já tê-lo registrado.
Nota
Este método de filtragem de dados é bastante arriscado porque os dados podem ser registrados na ordem errada, mas no nosso caso, isso não tem um papel significativo.
- O método
distinct()
remove duplicatas do fluxo. Em geral, isso pode ser útil se você precisar de elementos únicos no fluxo ou se quiser eliminar rapidamente duplicatas de uma lista. Você pode facilmente conseguir isso com a seguinte construção:
- O método
sorted
ordena todos os elementos do fluxo em ordem natural, do menor para o maior número ou em ordem alfabética. Isso também pode ser útil se você precisar de um fluxo ordenado ou se precisar ordenar rapidamente uma lista. - O método
skip(n)
ignora os primeirosn
elementos do fluxo. Isso pode ser útil ao trabalhar com arquivos de texto, onde as primeiras n linhas podem ser, por exemplo, metadados ou uma descrição do arquivo. Vale também mencionar o métodolimit(n)
, que geralmente limita o número de elementos no fluxo. Mesmo que criemos um fluxo com 1000 elementos e depois usemoslimit(200)
, o fluxo conterá apenas os primeiros 200 elementos.
main.java
Estes são os principais métodos intermediários que você precisará usar. Você pode explorar o restante dos métodos consultando a link para a documentação oficial do Java. Vamos prosseguir com os métodos terminais.
Métodos Terminais
- O método terminal com o qual você já está familiarizado é
toList()
. Ele converte o fluxo em uma lista e a retorna. Em outras palavras, podemos atribuir diretamente este fluxo com métodos a uma lista. Este método foi introduzido no Java 17 e serve como substituto para a construção mais complexacollect(Collectors.toList())
. - O método
collect()
também converte o fluxo em uma estrutura de dados específica. Ele utiliza, como parâmetro, um método da interfaceCollectors
. Esta interface possui métodos comotoList()
,toSet()
etoCollection()
. Por exemplo:
main.java
O método forEach()
recebe uma expressão lambda e realiza uma ação específica para cada elemento no stream.
Por exemplo:
main.java
A diferença entre este método e o método map
é que este método é terminal, e após utilizá-lo, não é possível chamar outros métodos.
Estes são todos os métodos básicos para trabalhar com streams. É um tópico complexo e talvez você não o compreenda imediatamente. Contudo, é um assunto que se domina com a prática. Nos próximos capítulos práticos sobre streams, você terá várias oportunidades para trabalhar com eles, já que é uma forma muito conveniente e prática de manipular listas e arrays de dados!
Tudo estava claro?