Manipulando arquivos com recursos do Java 8 - Blog da Caelum: desenvolvimento, web, mobile, UX e Scrum
Postado dia 14/07/2015 por Alexandre Aquiles em Programação 21
Há algum tempo, minha esposa pediu que eu descobrisse quantas horas semanais os servidores públicos federais trabalham. Essa informação está disponível no Portal de Transparência do Governo Federal.
Baixei um .csv com mais de 730 mil linhas e com quase 320 MB depois de descompactado. Estudei a estrutura do arquivo e resolvi fazer um programa Java para extrair as informações necessárias, utilizando alguns recursos do Java 8.
No Java 8, a classe java.nio.files.Files ganhou o método lines, que retorna um java.util.stream.Stream.
Temos que passar um java.nio.files.Path com o caminho do arquivo. Vamos utilizar a classe java.nio.files.Paths, para obter o arquivo 20140228_Cadastro.csv, que está na sub-pasta Downloads/201402_Servidores/ da minha pasta pessoal. A propriedade user.home contém o caminho da pasta pessoal do usuário.
Nosso lambda será responsável por aplicar a regex da constante HORA, utilizando um java.util.regex.Matcher para extrair a parte relevante da linha. Teremos o código:
Há um problema na nossa regex: ela deixa passar uma linha que fica vazia. Vamos filtrar o stream horasSemanais, deixando apenas as linhas não vazias.
Fazer isso parece complicado, né? Mas com o stream do Java 8, podemos usar o método Stream::collect, passando alguma implementação da interface funcional java.util.stream.Collector.
Na classe java.util.stream.Collectors (no plural), há uma implementação de Collector que faz agrupamentos. Exatamente o que a gente precisa! Para obtê-la, devemos chamar o método Collectors::groupingBy. Devemos passar dois lambdas, um para montar as chaves e outro para montar os valores do java.util.Map que será retornado pelo agrupador.
Uma maneira fácil de fazer isso é ordenar todas as linhas antes de agrupá-las, usando o método Stream::sorted:
Acontece que o Collectors::groupingBy cria um java.util.HashMap por padrão, que não garante que a ordem de inserção será mantida. Para manter a ordem do Map, podemos usar um java.util.LinkedHashMap.
69      12 HORAS SEMANAIS
23069   20 HORAS SEMANAIS
2216    24 HORAS SEMANAIS
1052    25 HORAS SEMANAIS
5509    30 HORAS SEMANAIS
2       32,5 HORAS SEMANAIS
1538    36 HORAS SEMANAIS
571777  40 HORAS SEMANAIS
7434    44 HORAS SEMANAIS
9325    60 HORAS SEMANAIS
308     66 HORAS SEMANAIS
115359  DEDICACAO EXCLUSIVA
Tags: Java, java8, lambda, stream
Michael Nascimento Santos 14/07/2015 at 09:45 #
Apenas uma melhoria, StandardCharsets.ISO_8859_1 já é uma constante pronta pra o exemplo que você deu. Seria divertido também comparar a performance deste exemplo com o uso de parallelStream().
Alberto 14/07/2015 at 10:24 #
Eita! Ótimo post! Adoro ver um pouco das novas APIs sendo usadas em casos realmente necessários.
Alexandre Aquiles 14/07/2015 at 10:51 #
Sugestões atendidas, Michel.
Troquei Files.lines(caminho, Charset.forName("ISO-8859-1")) para Files.lines(caminho, StandardCharsets.ISO_8859_1).
Sobre o uso de streams paralelas, um .parallel() após o Files.lines diminui em cerca de 30% o tempo para o usuário (real) com quase 2x mais tempo usando recursos do computador (user+sys).
Paulo Dias 14/07/2015 at 12:45 #
Rodrigo Peleias 14/07/2015 at 14:28 #
Parabéns pelo artigo!! Muito bom ver novas soluções com o Java 8!!
JOnh 15/07/2015 at 10:16 #
Gostaria de saber qual manipulação de dados e mais rápida. E utilizando os recursos do Java 8 ou Java 1.4
Chico Sokol 15/07/2015 at 11:02 #
Muito legal o post, parabéns, Alexandre!
Procurei como fazer a negação do isEmpty() de alguma maneira mais legível na hora de filtrar mas não achei nada na api do Java 8, seria legal se isso funcionasse:
Stream horasSemanaisNaoVazias = horasSemanais
.filter(String::empty.negate());
Mas não rola, acho que precisa fazer cast e aí perde o sentido. Pena eles não terem feito um Predicates.not(predicado) como existe no guava… O que você acha? Mais ficaria mais legível?
Além disso, reparei que está fazendo o sort de todas as linhas do arquivo, será que não seria mais eficiente ordenar só as chaves do Map final? Como você faria o código nesse caso? Não vejo alternativas sem deixar o código menos legível…
Eduardo Braga 15/07/2015 at 11:38 #
Começando de uma conversa informal e trazendo à tona a rapidez do Java 8. Estou começando com Java e gostei bastante da maneira como você estruturou o post, pude entender bem o que você fez. Parabéns!
Alexandre Aquiles 15/07/2015 at 12:06 #
Opa, Chico!
Realmente, a negação do isEmpty poderia ser mais simples. Dá pra fazer algo como filter(((Predicate) String::isEmpty).negate()). Mas fica muito feio! Tirando o cast para Predicate o código não compila, dando o erro "Method reference not expected here". Infelizmente, não dá pra usar um Method Reference diretamente, como se fosse um lambda. Uma outra maneira seria guardar o String::isEmpty em uma variável do tipo Predicate, logo no início do código. A solução de um Predicates.not parece uma boa!
Sobre o sorted(), bem observado! Uma versão que ordena as informações depois de agrupada seria mais eficiente. Só que não poderíamos usar o collect() com Collectors.groupingBy porque é uma operação terminal, que não retorna mais uma stream, mas um Map. A questão aqui é escolher uma boa implementação de Map. Se trocassemos de LinkedHashMap para TreeMap não precisaríamos mais fazer o sorted() na stream, porque o TreeMap já orderna por chave (exatamente o que a gente quer)! Vou deixar o código do post com o sorted() para explorar esse método das streams, mas coloquei sua sugestão em um gist!
Alexandre Aquiles 15/07/2015 at 14:16 #
JOnh, excelente questão!
Fiz uma pequena comparação utilizando recursos até o Java 1.3 com o pacote java.io, até o Java 1.7 com o pacote java.nio e também com os recursos do Java 8.
Compilei a partir da JDK 8 com a opção -source do javac para fixar as versões e executei na JRE 8.
Em termos de tempo de resposta, as versões antigona, a do Java 7 e do Java 8 com streams sequenciais tiveram performance semelhante, um pouquinho melhor em cada avanço na versão. Já uso de streams paralelas trouxe um ganho significativo na performance (cerca de 30% a menos), em relação às demais versões.
Em termos de implementação, os novos recursos foram levando a menos linhas de código e a mais legibilidade (na minha opinião, pelo menos).
Vale ressaltar que essas comparações não são nada definitivas. Uma comparação mais robusta deveria utilizar estatística e ferramentas como jmh ou caliper.
Guilherme Mendes 15/07/2015 at 21:53 #
Sei que não é exatamente o tema do artigo mas alguém sabe uma maneira de manipular RTF, mais precisamente concatená-lo utilizando Java? O que temos de mais atual para isso?
Alexandre Aquiles 15/07/2015 at 23:21 #
Nunca mexi com RTF em Java. É um formato antigo. Você precisa realmente usá-lo? Para .doc e .docx, o Apache POI é uma boa pedida.
De qualquer forma, uma busca no google me levou ao jRTF, que tem uma API muito boa. Só não sei se funciona bem…
vanderson 16/07/2015 at 13:47 #
Josimar Silva 17/07/2015 at 08:11 #
Excelente post. Primeira vez que vejo o uso massivo das novas features do Java 8 em um problema "real".
Rogerio J. Gentil 31/07/2015 at 09:54 #
O nome da publicação "Manipulando arquivos com recursos do Java 8" é modesto para um texto tão rico em explicação e uso de APIs do Java 8! Muito bom…!!!
Orlando 28/01/2016 at 16:09 #
abraao 21/05/2017 at 13:35 #
Parabéns pelo artigo. seria possivel usar a mesma stream para contar um outro atributo do arquivo.
Usar esse trecho duas vezes.
Alexandre Aquiles 22/05/2017 at 16:31 #
Um Stream::collect é uma operação terminal, isto é, que consome a Stream, sem que essa possa ser usada novamente.
Por isso, não é possível realizar dois collect seguidos.
Uma alternativa, se você quiser contar mais de um atributo, é implementar o seu próprio Collector.
Ana 05/06/2017 at 14:28 #
Se fosse para eu pegar o indíce de cada linha, como um List usamos o get(1), como faria usando Stream?
Alexandre Aquiles 11/06/2017 at 13:05 #
Não sei se entendi muito bem, mas um exemplo comum é usar um IntStream, chamando .get(i) em uma lista e criando objetos que serão coletados em outra:
List<A> listaA = //...
List<B> listaB = IntStream.range(0, listaA.size())
.mapToObj(i -> new B(i, listaA.get(i))
Para esse caso, talvez o velho for com a variável i fique mais legível.
Wallace Sousa 04/09/2019 at 16:00 #
Simplesmente excepcional! Eu testei com os dados de 07 de 2019, mais de 780.500 linhas com uma máquina com 16Gb de RAM, Core i7, 2.90GHz de processador, Win10 64bits e gastou 4 segundos.
Esse tipo de processamento é bastante útil hoje em dia e com essa velocidade é um ganho de tempo excelente, principalmente para dashboards.