Funcionalidades do JDK 8 (Java 8) - Parte IX - API de Streams
Aplicação da API de Streams Parte I
Nessa parte final sobre a API de streams do Java 8, gostaria de fazer um exemplo prático utilizando o que vimos até o momento
Utilizando a classe java.nio.file.Files
que entrou no Java 7 para facilitar a manipulação de arquivos e diretórios trabalhando com a interface Path
vamos tentar listar todos os arquivos em um diretório.
Saiba que a classe java.nio.file.Files
possui métodos para trabalhar com Stream
e que basta apenas pegar o Stream<Path>
e depois percorrer com um forEach()
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
private static final String ROOT_PATH = "/home/seujorge";
public static void main(String[] args) throws IOException {
Files.list(Paths.get(ROOT_PATH))
.forEach(System.out::println);
}
}
Agora vamos tentar obter apenas os arquivos:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
private static final String ROOT_PATH = "/home/seujorge";
public static void main(String[] args) throws IOException {
Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().contains(".") || path.toString().startsWith("."))
.forEach(System.out::println);
}
}
Vamos criar um arquivo e tentar ler os dados desse arquivo:
Para exemplo eu crieei esse arquivo aqui, e chamei de sources.txt
mas você pode utilizar qualquer outro se quiser:
Agora crie o arquivo meu_arquivo.txt coloque um conteúdo nele e tente fazer a leitura do arquivo
No meu caso crie o seguinte arquivo:
```txt
seujorge
/etc/apt/sources.list
# deb cdrom:[Ubuntu 22.04.2 LTS _Jammy Jellyfish_ - Release amd64 (20230223)]/ jammy main restricted
# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
# newer versions of the distribution.
deb http://br.archive.ubuntu.com/ubuntu/ jammy main restricted
# deb-src http://br.archive.ubuntu.com/ubuntu/ jammy main restricted
## Major bug fix updates produced after the final release of the
## distribution.
deb http://br.archive.ubuntu.com/ubuntu/ jammy-updates main restricted
# deb-src http://br.archive.ubuntu.com/ubuntu/ jammy-updates main restricted
## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu
`tu.com/ubuntu/ jammy-updates universe
# deb-src http://br.archive.ubuntu.com/ubuntu/ jammy-updates universe
Se você tentou fazer assim
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
private static final String ROOT_PATH = "/home/seujorge";
public static void main(String[] args) throws IOException {
Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().endsWith(".txt"))
.map(path -> Files.lines(path))
.forEach(System.out::println);
}
}
Saiba que esse código não vai compilar ! Isso por que Files.lines()
lança uma IOEXception
bem como Files.list()
, o problema do Files.list()
é fácil de resolver basta envolver o código em um try...catch
, para manter esse exemplo simples, eu apenas adicionei a exception ao metodo main()
mas obviamente isso não é recomendável.
Agora o problema do Files.lines()
é complicado pois a implementação da lambda que lança a exception IOException
e o método map()
recebe uma Function
, essa function, tem o método apply()
e esse método não lança exception em sua assinatura, eis o problema !
Diante disso há duas soluções:
-
Escrever uma classe anônima ou uma lambda definida com as chaves e com o try/catch por dentro.
-
Escrever um método estático simples, que faz o wrap da chamada para evitar a checked exception.
Como eu sou preguiçoso vou optar pela segunda opção ! Sendo assim vamos escrever o seguinte método:
static Stream<String> lines(Path path) {
try {
return Files.lines(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
E agora invocar ele no nosso código escrito anteriormente
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class Main {
private static final String ROOT_PATH = "/home/seujorge";
public static void main(String[] args) throws IOException {
Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().endsWith(".txt"))
.map(path -> lines(path))
.forEach(System.out::println);
}
static Stream<String> lines(Path path) {
try {
return Files.lines(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Esse código até compila, mas se você tentou executar viu que a saí ficou um pouco estranha, algo mais ou menos assim:
java.util.stream.ReferencePipeline$Head@7ba4f24f
Isso acontece por que o nosso método lines()
retorna um Stream<String>
e o map()
vai produzir um Stream<String>
, logo teremos um Stream<Stream<String>>
, você pode ver isso retirando o forEach()
e atribuindo o resultado do map()
a uma variável
Stream<Stream<String>> streamStream = Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().endsWith(".txt"))
.map(path -> lines(path));
Sendo assim, como resolver esse problema ?
Achatando um stream com flatMap
Um flatMap()
vai "achatar" o nosso stream de streams, para utilizar basta apenas trocar a invocação no final do Stream<String>
Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().endsWith(".txt"))
.flatMap(path -> lines(path))
.forEach(System.out::println);
Então nosso código todo fica assim:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class Main {
private static final String ROOT_PATH = "/home/seujorge";
public static void main(String[] args) throws IOException {
Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().endsWith(".txt"))
.flatMap(path -> lines(path))
.forEach(System.out::println);
}
static Stream<String> lines(Path path) {
try {
return Files.lines(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Pra que serve o flatMap()
a final ?
Utilizamos o flatMap()
sempre que precisamos trabalhar com coleções de coleções e quermos que o resultado da transformação seja reduzido a um stream simples, sem composição.
Vamos a um outro exemplo prático:
Voltando ao nosso exemplo com pilotos, vamos imaginar que temos grupos de pilotos, para isso vamos criar uma classe para representar essa abstração de grupo
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class Grupo {
private Set<Piloto> pilotos = new HashSet<>();
public void add(Piloto piloto) {
pilotos.add(piloto);
}
public Set<Piloto> getPilotos() {
return Collections.unmodifiableSet(this.pilotos);
}
}
Agora iamgina que tenhamos grupos, separando quem fala inglês e quem fala espanhol
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Grupo englishSpeakers = new Grupo();
englishSpeakers.add(new Piloto("Senna", 1000));
englishSpeakers.add(new Piloto("Alesi", 200));
Grupo spanishSpeakers = new Grupo();
spanishSpeakers.add(new Piloto("Albon", 983));
spanishSpeakers.add(new Piloto("Prost", 100));
}
}
Vamos colocar esses grupos dentro de uma coleção
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) throws IOException {
Grupo englishSpeakers = new Grupo();
englishSpeakers.add(new Piloto("Senna", 1000));
englishSpeakers.add(new Piloto("Alesi", 200));
Grupo spanishSpeakers = new Grupo();
spanishSpeakers.add(new Piloto("Albon", 983));
spanishSpeakers.add(new Piloto("Prost", 100));
List<Grupo> grupos = Arrays.asList(englishSpeakers, spanishSpeakers);
}
}
Se tentarmos obter todos os pilotos desses grupos com algo como
grupos.stream()
.map(grupo -> grupo.getPilotos().stream());
Vamos obter um indesejado Stream<Stream<Piloto>>
daí o flatMap()
pode ser utilizado para "desembrulhar" esses streams achantando eles:
grupos.stream()
.flatMap(grupo -> grupo.getPilotos().stream())
.distinct()
.forEach(System.out::println);
O código todo fica assim:
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) throws IOException {
Grupo englishSpeakers = new Grupo();
englishSpeakers.add(new Piloto("Senna", 1000));
englishSpeakers.add(new Piloto("Alesi", 200));
Grupo spanishSpeakers = new Grupo();
spanishSpeakers.add(new Piloto("Albon", 983));
spanishSpeakers.add(new Piloto("Prost", 100));
List<Grupo> grupos = Arrays.asList(englishSpeakers, spanishSpeakers);
grupos.stream()
.flatMap(grupo -> grupo.getPilotos().stream())
.distinct()
.forEach(System.out::println);
}
}
Dessa forma obtemos todos os usuários de ambos os grupos, sem repetição, outra forma de fazer isso seria coletar o resultado da pipeline com em um Set
, daí não seria preciso o distinct()
.