Java Streams API, manipulando coleções de maneira fácil
Ao desenvolver aplicações, é frequente a necessidade de manipular coleções de dados, realizando operações como filtragem, redução e transformação de valores. Entretanto, abordagens imperativas podem resultar em um código mais extenso e de difícil leitura.
A partir do Java 8, a Stream API introduziu uma abordagem declarativa para manipulação de coleções, proporcionando um código mais conciso, fácil de manter e potencialmente mais eficiente, graças ao suporte ao processamento paralelo. No Java, uma Stream representa uma sequência de elementos e atua como um pipeline para seu processamento.
Vamos comparar rapidamente as duas abordagens, começando pelo método imperativo.
public class Produto {
private String nome;
private Double preco;
private Tipo tipo;
// gets sets e construtor
}
List<Produto> produtos = Arrays.asList(
new Produto("Refrigerante 2L", 10.00, Produto.Tipo.BEBIDA),
new Produto("Pizza 4 Queijos", 70.00, Produto.Tipo.COMIDA),
new Produto("Sorvete Pote", 35.00, Produto.Tipo.SOBREMESA),
new Produto("Suco", 15.00, Produto.Tipo.BEBIDA),
new Produto("Porção batata frita", 25.00, Produto.Tipo.COMIDA)
);
List<Produto> comidas = new ArrayList<Produto>();
for(Produto p : produtos){
if(p.getTipo() == Produto.Tipo.COMIDA){
comidas.add(p);
}
}
Collections.sort(comidas, new Comparator<Produto>() {
@Override
public int compare(Produto p1, Produto p2) {
return Double.compare(p1.getPreco(), p2.getPreco());
}
});
for(Produto p : comidas){
System.out.println(p.getNome()+ " - "+p.getPreco());
}
Agora, vamos ver a abordagem utilizando Stream.
List<Produto> comidas = produtos.stream()
.filter( p -> p.getTipo() == Produto.Tipo.COMIDA)
.sorted( (p1, p2) -> Double.compare(p1.getPreco(), p2.getPreco() ) )
.collect( Collectors.toList() );
comidas.forEach( c -> {
System.out.println(c.getNome()+ " - " + c.getPreco());
});
Trabalhando com Stream
O uso de Streams em Java geralmente segue três etapas: criação, operações intermediárias e operações terminais.

Criação
As Streams em Java podem ser criadas a partir de coleções, arrays, operações de I/O ou métodos fornecidos pela própria interface Stream. As coleções possuem o método stream()
, que retorna um objeto Stream do mesmo tipo da coleção original. Além disso, o método Stream.of()
permite criar uma Stream a partir de valores ou objetos passados como parâmetros. A classe Files
, por sua vez, disponibiliza o método lines()
, que retorna um Stream de strings contendo as linhas de um arquivo.
List<Integer> x = Arrays.asList(1,4,7,8,5,2,3,6,9);
Stream<Integer> xStream = x.stream();
Set<String> nomes = new HashSet<String>(Arrays.asList("Rodrigo", "Rochele", "Anderson"));
Stream<String> nomesStream = nomes.stream();
Stream<Integer> numerosStream = Stream.of(1, 2, 3, 4, 5);
Stream<String> linhas = Files.lines( Paths.get("file.txt") );
Outra maneira de criar uma Stream a partir de uma Collection é usar o método parallelStream()
, que permite paralelizar o processamento, proporcionando maior eficiência na execução.
Operações Interemediárias
As operações intermediárias são aquelas que transformam o fluxo de elementos de alguma forma. Elas podem ser encadeadas, pois cada método retorna um objeto Stream. Essas operações são “avaliadas de forma preguiçosa” (lazy evaluation), ou seja, não são executadas até que uma operação terminal seja invocada.
Algumas das operações intermediárias incluem:
filter(Predicate<T>)
Filtra elementos com base em uma condição booleana. O método filter
recebe um objeto que implementa a interface Predicate
(java.util.function.Predicate), a qual define um método que recebe um elemento do tipo da Stream e retorna um valor booleano. Se o valor for true
, o elemento será mantido para a próxima operação da Stream; se for false
, o elemento será ignorado.
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10 );
List<Integer> pares = numeros.stream().filter( new Predicate<Integer>() {
@Override
public boolean test(Integer value) {
return value % 2 == 0;
}
}).toList();
System.out.println(pares); // saída: [2, 4, 6, 8, 10]
Como você pode ter percebido, a criação de um objeto inline torna o código mais verboso. Uma abordagem melhor é utilizar expressões lambda. Vamos agora ver a implementação do mesmo exemplo utilizando lambdas.
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10 );
List<Integer> pares = numeros.stream()
.filter(n -> n % 2 == 0)
.toList();
System.out.println(pares); // saída: [2, 4, 6, 8, 10]
map(Function<T, R>)
Transforma cada elemento da Stream em outro valor. O método recebe um objeto da interface Function
, que define dois tipos genéricos: o primeiro representa o tipo dos elementos da Stream, e o segundo, o tipo para o qual o elemento será convertido. A interface contém o método apply
, que recebe o elemento a ser transformado e retorna o elemento após a transformação.
List<String> nomes = Arrays.asList("rodrigo","Thais", "Amanda", "Lucio");
List<String> nomesMaiusculos = nomes.stream().map( new Function<String, String>() {
@Override
public String apply(String value) {
return value.toUpperCase();
}
}).toList();
System.out.println(nomesMaiusculos); // Saída: [RODRIGO, THAIS, AMANDA, LUCIO]
Agora, utilizando expressões lambda.
List<String> nomes = Arrays.asList("rodrigo","Thais", "Amanda", "Lucio");
List<String> nomesMaiusculos = nomes.stream()
.map( n -> n.toUpperCase())
.toList();
System.out.println(nomesMaiusculos); // Saída: [RODRIGO, THAIS, AMANDA, LUCIO]
Também é importante destacar que este método é bastante útil para transformar uma coleção de objetos em tipos primitivos, ou vice-versa, além de possibilitar a conversão de um tipo de objeto para outro, como, por exemplo, de uma classe de entidade para um DTO.
List<Produto> produtos = Arrays.asList(
new Produto("Refrigerante 2L", 10.00, Produto.Tipo.BEBIDA),
new Produto("Pizza 4 Queijos", 70.00, Produto.Tipo.COMIDA),
new Produto("Sorvete Pote", 35.00, Produto.Tipo.SOBREMESA),
new Produto("Suco", 15.00, Produto.Tipo.BEBIDA),
new Produto("Porção batata frita", 25.00, Produto.Tipo.COMIDA)
);
NumberFormat nf = NumberFormat.getCurrencyInstance( Locale.forLanguageTag("pt-BR"));
List<String> nomesProdutos = produtos.stream()
.map( n -> n.getNome() + "(" +nf.format(n.getPreco())+")" )
.toList();
System.out.println(nomesProdutos);
// Saída:
// [Refrigerante 2L(R$ 10,00), Pizza 4 Queijos(R$ 70,00), Sorvete Pote(R$ 35,00), Suco(R$ 15,00), Porção batata frita(R$ 25,00)]
distinct()
Remove elementos duplicados da Stream.
List<Double> precos = Arrays.asList(10.0, 10.2, 10.0, 30.0, 40.5, 30.0);
List<Double> precosUnicios = precos.stream().distinct().toList();
System.out.println(precosUnicios); // Saída: [10.0, 10.2, 30.0, 40.5]
sorted() ou sorted(Comparator<T>)
Ordena os elementos da Stream de forma natural ou com um comparador.
List<Integer> numeros = Arrays.asList(10, 5, 7, 3, 9, -1);
List<Integer> ordenados = numeros
.stream()
.sorted()
.toList();
System.out.println(ordenados); // Saída: [-1, 3, 5, 7, 9, 10]
Para ordenar usando um comparador, devemos fornecer como parâmetro um objeto que implemente a interface Comparable
, sobrescrevendo o método compare
. Esse método recebe dois objetos/valores a serem comparados e deve retornar um valor negativo se o primeiro objeto for menor que o segundo, 0 se forem iguais e um valor positivo se o primeiro objeto for maior que o segundo.
List<Produto> produtos = Arrays.asList(
new Produto("Refrigerante 2L", 10.00, Produto.Tipo.BEBIDA),
new Produto("Pizza 4 Queijos", 70.00, Produto.Tipo.COMIDA),
new Produto("Sorvete Pote", 35.00, Produto.Tipo.SOBREMESA),
new Produto("Suco", 15.00, Produto.Tipo.BEBIDA),
new Produto("Porção batata frita", 25.00, Produto.Tipo.COMIDA)
);
List<Produto> prodOrdPre = produtos
.stream()
.sorted( new Comparator<Produto>() {
@Override
public int compare(Produto p1, Produto p2) {
return (int) (p1.getPreco() - p2.getPreco());
}
})
.toList();
for(Produto p : prodOrdPre){
System.out.println(p.getNome() + " - " +p.getPreco());
}
// Saída:
//Refrigerante 2L - 10.0
// Suco - 15.0
// Porção batata frita - 25.0
// Sorvete Pote - 35.0
// Pizza 4 Queijos - 70.0
Agora, um exemplo utilizando expressões lambda.
List<Produto> prodOrdPre = produtos.stream()
.sorted( (p1, p2) -> (int) (p1.getPreco() - p2.getPreco()) )
.toList();
for(Produto p : prodOrdPre){
System.out.println(p.getNome() + " - " +p.getPreco());
}
// Saída:
//Refrigerante 2L - 10.0
// Suco - 15.0
// Porção batata frita - 25.0
// Sorvete Pote - 35.0
// Pizza 4 Queijos - 70.0
limit(long n)
Restringe a Stream aos primeiros n
elementos.
List<Integer> nums = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
List<Integer> nums5Primeiros = nums.stream().limit(5).toList();
System.out.println(nums5Primeiros); //Saída: [1, 2, 3, 4, 5]
skip(long)
Pula os primeiros n
elementos da Stream e retorna o restante.
List<Integer> nums = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
List<Integer> numsSkip = nums.stream().skip(5).toList();
System.out.println(numsSkip);// Saída: [6, 7, 8, 9, 10]
flatMap(Function<T, Stream<R>)
Transforma cada elemento de uma Stream em outra Stream e, em seguida, “achata” (flatten) todas essas Streams em uma única Stream. .
Diferente do map()
, que apenas transforma elementos um por um, flatMap()
expande os elementos, permitindo trabalhar com estruturas aninhadas, como listas dentro de listas.
List<List<String>> pedidos = Arrays.asList(
Arrays.asList("Pizza", "Refrigerante", "Batata Frita"),
Arrays.asList("X Fritas", "Suco", "Pudim"),
Arrays.asList("Pizza", "Suco", "Pudim"),
Arrays.asList("Batata Frita", "Refrigerante")
);
List<String> produtosList = pedidos.stream()
.flatMap( new Function<List<String>,Stream<String>>() {
@Override
public Stream<String> apply(List<String> value) {
return value.stream();
}
})
.toList();
System.out.println(produtosList);
// Saída:
// [Pizza, Refrigerante, Batata Frita, X Fritas, Suco, Pudim, Pizza, Suco, Pudim, Batata Frita, Refrigerante]
List<List<String>> pedidos = Arrays.asList(
Arrays.asList("Pizza", "Refrigerante", "Batata Frita"),
Arrays.asList("X Fritas", "Suco", "Pudim"),
Arrays.asList("Pizza", "Suco", "Pudim"),
Arrays.asList("Batata Frita", "Refrigerante")
);
List<String> produtosList= pedidos.stream()
.flatMap( value -> value.stream() )
.toList();
System.out.println(produtosList);
// Saída:
// [Pizza, Refrigerante, Batata Frita, X Fritas, Suco, Pudim, Pizza, Suco, Pudim, Batata Frita, Refrigerante]
public class Pedido {
private int id;
private List<String> produtos;
// contrutor, gets e sets
}
List<Pedido> pedidos = Arrays.asList(
new Pedido(1, Arrays.asList("Notebook", "Mouse", "Teclado") ),
new Pedido(2, Arrays.asList("PC", "Cadeira Gamer", "Mouse Gamer") ),
new Pedido(3, Arrays.asList("Celular") )
);
List<String> produtos = pedidos
.stream()
.flatMap( pedido -> pedido.getProdutos().stream())
.toList();
System.out.println(produtos);
// Saída:
// [Notebook, Mouse, Teclado, PC, Cadeira Gamer, Mouse Gamer, Celular]
Operações terminais
As operações terminais são ações que iniciam o processamento da Stream e geram um resultado. Algumas dessas operações incluem:
forEach(Consumer<T>)
Executa uma ação para cada elemento da Stream. O método recebe um objeto da interface Consumer
, que sobrescreve o método accept
, o qual é chamado para cada elemento da coleção, recebendo o elemento como parâmetro.
List<String> nomes = Arrays.asList("Rodrigo", "Carla", "Julio", "Anderson");
nomes.stream().forEach( new Consumer<String>() {
@Override
public void accept(String nome) {
System.out.println(nome);
}
} );
// Saída:
// Rodrigo
// Carla
// Julio
// Anderson
Como sempre, podemos utilizar apenas expressões lambda.
List<String> nomes = Arrays.asList("Rodrigo", "Carla", "Julio", "Anderson");
nomes.stream().forEach( nome -> System.out.println(nome) );
Passando uma referência ao método ao invés de uma lambda
Ainda poderíamos fazer de outra forma, já que a única ação executada no forEach
é chamar um método que recebe o mesmo parâmetro da lambda. Nesse caso, podemos simplesmente passar a referência ao método, utilizando o objeto seguido de ::
ao invés do operador de ponto (.
) e o nome do método sem os parênteses.
List<String> nomes = Arrays.asList("Rodrigo", "Carla", "Julio", "Anderson");
nomes.stream().forEach( System.out::println );
E, é claro, podemos criar nossos próprios métodos para passá-los como referência.
public class ForEach {
public static void main(String[] args) {
List<String> nomes = Arrays.asList("Rodrigo", "Carla", "Julio", "Anderson");
ForEach obj = new ForEach();
nomes.stream().forEach(obj::exibir );
}
public void exibir(String nome){
System.out.println("Nome: "+ nome);
}
}
collect(Collector<T, A, R>)
Coleta os elementos da stream em uma coleção ou outro tipo de estrutura de dados os retornando . Ele recebe um objeto da interface Collector, que define como os elementos serão acumulados, transformados e combinados. O método collect()
é frequentemente usado para converter uma stream em uma Lista (List
), Conjunto (Set
) ou Mapa (Map
), utilizando Collectors
pré-definidos ou personalizados.
// Stream para List
List<Integer> numeros = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
List<Integer> impares = numeros.stream()
.filter( n -> n % 2 != 0 )
.collect(Collectors.toList());
System.out.println(impares);
//Stream para Set
List<Integer> numeros = Arrays.asList(1,1,2,2,3,3,4,4,5,5,6,7,8,9,10);
Set<Integer> impares = numeros.stream()
.filter( n -> n % 2 != 0 )
.collect(Collectors.toSet());
System.out.println(impares);
// Objeto para String
List<Produto> produtos = Arrays.asList(
new Produto("Refrigerante 2L", 10.00, Produto.Tipo.BEBIDA),
new Produto("Pizza 4 Queijos", 70.00, Produto.Tipo.COMIDA),
new Produto("Sorvete Pote", 35.00, Produto.Tipo.SOBREMESA),
new Produto("Suco", 15.00, Produto.Tipo.BEBIDA),
new Produto("Porção batata frita", 25.00, Produto.Tipo.COMIDA)
);
Map<String, Double> mapa = produtos.stream()
.filter( p -> p.getTipo() == Produto.Tipo.COMIDA)
.collect(Collectors.toMap(Produto::getNome, Produto::getPreco));
System.out.println(mapa);
// Saída:
// {Pizza 4 Queijos=70.0, Porção batata frita=25.0}
toList()
Como o uso de collect(Collectors.toList())
é bastante comum, no Java 16 e versões posteriores, é possível simplificar e utilizar apenas toList()
para converter a Stream em uma lista.
List<Integer> numeros = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
List<Integer> impares = numeros.stream()
.filter( n -> n % 2 != 0 )
.toList();
System.out.println(impares);
reduce(T, BinaryOperator<T>)
Combina os elementos da stream em um único valor. Ele recebe um objeto da interface BinaryOperator, que sobrescreve o método apply, responsável por aplicar a operação de redução a dois elementos da coleção, sucessivamente, até restar apenas um resultado final.
O método apply(T t, T u)
recebe dois argumentos do mesmo tipo T
e retorna um único resultado também do tipo T
. Na primeira chamada, ele recebe o primeiro parâmetro de reduce
e o primeiro valor da Stream como segundo parâmetro. Esse valor se torna o primeiro parâmetro do apply
na segunda chamada, e o segundo valor da Stream é passado como segundo parâmetro, repetindo esse processo até a conclusão do processamento.
List<Integer> numeros = Arrays.asList(1,2,3,4,5,6,7,8,9);
int total = numeros
.stream()
.reduce(0, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer acc, Integer v) {
return acc + v;
}
});
System.out.println(total); // Saída: 45
List<Integer> numeros = Arrays.asList(1,2,3,4,5,6,7,8,9);
int total = numeros
.stream()
.reduce(0, (acc , value) -> {return acc + value; } );
System.out.println(total);
List<String> nomes = Arrays.asList("Rodrigo","Marcos", "Joana", "Michael");
String lis = nomes.stream()
.reduce("", (acc, value) -> { return acc + "<li>"+value+"</li>\n"; });
System.out.println(lis);
// Saída:
// <li>Rodrigo</li>
// <li>Marcos</li>
// <li>Joana</li>
// <li>Michael</li>
count()
Retorna o número de elementos na stream como um long
.
List<Integer> numeros = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
long totaNumeroImpares = numeros
.stream()
.filter( n -> n % 2 != 0 )
.count();
System.out.println(totaNumeroImpares); // Saída: 5
min(Comparator<T>)
Retorna o menor elemento da stream, baseado em um Comparator
. O retorno é um objeto Option<T>
.
List<Integer> numeros = Arrays.asList(20, 15, 8, -5, 13, 16,3, 22, 42);
Optional<Integer> menor = numeros
.stream()
.min( (n1, n2) -> Integer.compare(n1, n2));
System.out.println(menor.get());// Saída: -5
Lembre-se de que também podemos passar uma referência a um método. Por exemplo, o método de comparação Integer.compare
recebe os mesmos parâmetros que um Comparator
.
List<Integer> numeros = Arrays.asList(20, 15, 8, -5, 13, 16,3, 22, 42);
Optional<Integer> menor = numeros
.stream()
.min( Integer::compare );
System.out.println(menor.get());// Saída: -5
max(Comparator<T>)
Retorna o maior elemento da stream, baseado em um Comparator
.
List<Integer> numeros = Arrays.asList(20, 15, 8, -5, 13, 16,3, 22, 42);
Optional<Integer> maior = numeros
.stream()
.max( (n1, n2) -> Integer.compare(n1, n2));
System.out.println(maior.get()); // Saída: 42
toArray()
Converte a Stream em um array. Para isso, devemos passar uma referência ao construtor do tipo de objeto desejado para o método toArray
, caso contrário, ele retornará um array do tipo Object
.
List<String> nomes = Arrays.asList("Rodrigo", "Otavio", "Henrique", "Alexandra");
String[] nomesArr = (String[]) nomes.stream().toArray(String[]::new);
for(String n: nomesArr){
System.out.println(n);
}
anyMatch(Predicate<T>)
Retorna true
se algum elemento atender à condição.
List<Produto> produtos = Arrays.asList(
new Produto("Refrigerante 2L", 10.00, Produto.Tipo.BEBIDA),
new Produto("Pizza 4 Queijos", 70.00, Produto.Tipo.COMIDA),
new Produto("Sorvete Pote", 35.00, Produto.Tipo.SOBREMESA),
new Produto("Suco", 15.00, Produto.Tipo.BEBIDA),
new Produto("Porção batata frita", 25.00, Produto.Tipo.COMIDA)
);
boolean existeSobremesa = produtos
.stream()
.anyMatch( p -> p.getTipo() == Produto.Tipo.SOBREMESA);
if(existeSobremesa){
System.out.println("Tem sobremesa na lista de produtos");
}else{
System.out.println("Não tem sobremesa na lista de produtos");
}
allMatch(Predicate<T>)
Retorna true
se todos os elementos atenderem à condição.
List<Produto> produtos = Arrays.asList(
new Produto("Refrigerante 2L", 10.00, Produto.Tipo.BEBIDA),
new Produto("Pizza 4 Queijos", 70.00, Produto.Tipo.COMIDA),
new Produto("Sorvete Pote", 35.00, Produto.Tipo.SOBREMESA),
new Produto("Suco", 15.00, Produto.Tipo.BEBIDA),
new Produto("Porção batata frita", 25.00, Produto.Tipo.COMIDA)
);
boolean existeSobremesa = produtos
.stream()
.allMatch( p -> p.getTipo() == Produto.Tipo.COMIDA);
if(existeSobremesa){
System.out.println("Todos elementos na lista de produtos são COMIDA");
}else{
System.out.println("Nem todos elementos na lista de produtos são COMIDA");
}
findFirst()
Retorna o primeiro elemento da stream como Optional<T>
.
List<String> nomes = Arrays.asList("Rodrigo", "Flavia", "Anelise", "Diego");
Optional<String> primeiro = nomes.stream().findAny();
if(primeiro.isPresent()){
System.out.println(primeiro.get());
}
As Streams são um recurso poderoso da linguagem de programação Java, oferecendo uma maneira elegante e eficiente de processar coleções. Ao utilizar streams, é possível escrever códigos mais legíveis, expressivos e, potencialmente, paralelizados. Compreender e dominar os conceitos básicos de streams certamente aumentará sua produtividade como desenvolvedor Java.
Espero que esta pequena introdução tenha sido util. T++