Java Streams
Um stream ("fluxo", "correnteza") é uma linha de montagem de dados: os elementos de uma coleção entram por uma ponta, passam por estações (filtrar, transformar, ordenar…) e, no fim, viram um resultado (uma lista, uma soma, uma contagem). Em vez de escrever o laço que faz tudo isso, você descreve as estações, cada uma com uma lambda. É a parte do Java moderno que mais muda a cara do código.
- O que é um stream e como ele se compara a um laço.
- Criar streams:
lista.stream(),Stream.of,Arrays.stream,IntStream.range. - As operações intermediárias:
filter,map,sorted,distinct,limit,skip. - As operações terminais:
toList,forEach,count,anyMatch,findFirst,reduce. - Somas e médias com
IntStream:sum,average. - A preguiça: nada roda sem operação terminal (com demonstração real).
- Por que um stream não pode ser usado duas vezes (
IllegalStateException).
A ideia: uma linha de montagem
Pense numa fábrica de sucos. As laranjas entram na esteira; a primeira estação descarta as estragadas; a segunda transforma cada laranja em suco; a terceira engarrafa. Ninguém carrega a caixa inteira de uma estação para a outra: cada laranja vai passando, uma de cada vez.
turma ──► filter(nota ≥ 7) ──► map(nome em maiúsculas) ──► sorted() ──► toList()
fonte estação: descarta estação: transforma estação resultado
quem não passou cada aluno em um texto ordena (terminal)
Todo stream tem três partes:
- uma fonte: de onde vêm os elementos (uma lista, um array, um intervalo de números);
- zero ou mais operações intermediárias: as estações; cada uma devolve um novo stream;
- uma operação terminal: a que produz o resultado e "liga a esteira".
Um detalhe importante: o stream não guarda os dados e não altera a fonte. A lista original continua igual; o resultado é sempre uma coisa nova.
Laço × stream
A Ana quer os nomes dos aprovados (nota 7 ou mais), em maiúsculas e em ordem alfabética. Veja os dois jeitos:
import java.util.ArrayList;
import java.util.List;
record Aluno(String nome, double nota) { }
public class Main {
public static void main(String[] args) {
List<Aluno> turma = List.of(
new Aluno("Carla", 9.5), new Aluno("Edu", 4.0), new Aluno("Ana", 8.0),
new Aluno("Duda", 6.5), new Aluno("Bruno", 7.0));
// Com laço
List<String> aprovados1 = new ArrayList<>();
for (Aluno a : turma) {
if (a.nota() >= 7) {
aprovados1.add(a.nome().toUpperCase());
}
}
aprovados1.sort(null);
System.out.println("laço: " + aprovados1);
// Com stream
List<String> aprovados2 = turma.stream()
.filter(a -> a.nota() >= 7)
.map(a -> a.nome().toUpperCase())
.sorted()
.toList();
System.out.println("stream: " + aprovados2);
}
}
$ javac Main.java $ java Main laço: [ANA, BRUNO, CARLA] stream: [ANA, BRUNO, CARLA]
- turma.stream()
- A fonte: abre um stream com os alunos da lista.
- .filter(a -> a.nota() >= 7)
- Filtrar: só passa quem satisfaz a condição (um
Predicate). - .map(a -> a.nome().toUpperCase())
- Mapear: transforma cada aluno num texto (uma
Function). Depois dele, o stream é deString, não mais deAluno. - .sorted()
- Ordena (ordem natural).
- .toList()
- A operação terminal: junta tudo numa lista Java 16+. Em versões antigas:
.collect(Collectors.toList())(veja Collectors).
O resultado é o mesmo. A diferença é o estilo: o laço diz como fazer (crie uma lista, percorra, teste, acrescente, ordene); o stream diz o que você quer (filtre os aprovados, pegue os nomes, ordene). Por convenção, cada operação fica numa linha, começando com o ponto, para ler de cima para baixo como uma receita.
Streams brilham em "filtrar, transformar, juntar". Para lógicas com muitos if, break, ou que mexem em várias variáveis ao mesmo tempo, um laço comum costuma ser mais claro. Os dois são Java moderno.
Criar um stream
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
List<String> frutas = List.of("uva", "kiwi", "pera");
System.out.println(frutas.stream().toList());
System.out.println(Stream.of("a", "b", "c").toList());
int[] notas = {7, 10, 3};
System.out.println(Arrays.stream(notas).sum());
System.out.println(IntStream.range(1, 5).boxed().toList());
System.out.println(IntStream.rangeClosed(1, 5).boxed().toList());
System.out.println(Stream.iterate(1, x -> x * 2).limit(6).toList());
}
}
$ javac Main.java $ java Main [uva, kiwi, pera] [a, b, c] 20 [1, 2, 3, 4] « range: o 5 fica de fora [1, 2, 3, 4, 5] « rangeClosed: inclui o 5 [1, 2, 4, 8, 16, 32]
| Fonte | Cria |
|---|---|
colecao.stream() | Stream com os elementos de qualquer List, Set… (para um Map: mapa.entrySet().stream()) |
Stream.of(a, b, c) | Stream com esses valores |
Arrays.stream(array) | Stream com os elementos de um array (com int[], um IntStream) |
IntStream.range(a, b) | Os inteiros de a até b - 1 |
IntStream.rangeClosed(a, b) | Os inteiros de a até b |
Stream.iterate(inicio, regra) | Sequência infinita (1, 2, 4, 8…): sempre com limit |
O .boxed() converte um IntStream (de int) num Stream<Integer>, para poder virar uma List<Integer> (lembre: coleções não guardam primitivos).
Operações intermediárias: as estações
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> notas = List.of(7, 10, 3, 8, 5, 10, 7, 9);
System.out.println("original: " + notas);
System.out.println("filter(n >= 7): " + notas.stream().filter(n -> n >= 7).toList());
System.out.println("map(n * 10): " + notas.stream().map(n -> n * 10).toList());
System.out.println("map(\"nota \" + n): " + notas.stream().map(n -> "nota " + n).limit(2).toList());
System.out.println("sorted(): " + notas.stream().sorted().toList());
System.out.println("distinct(): " + notas.stream().distinct().toList());
System.out.println("limit(3): " + notas.stream().limit(3).toList());
System.out.println("skip(5): " + notas.stream().skip(5).toList());
System.out.println("tudo junto: " + notas.stream()
.distinct()
.filter(n -> n >= 7)
.sorted(Comparator.reverseOrder())
.limit(2)
.toList());
}
}
$ javac Main.java
$ java Main
original: [7, 10, 3, 8, 5, 10, 7, 9]
filter(n >= 7): [7, 10, 8, 10, 7, 9]
map(n * 10): [70, 100, 30, 80, 50, 100, 70, 90]
map("nota " + n): [nota 7, nota 10] « map pode mudar o tipo: Integer → String
sorted(): [3, 5, 7, 7, 8, 9, 10, 10]
distinct(): [7, 10, 3, 8, 5, 9] « tira repetidos, mantém a 1ª ocorrência
limit(3): [7, 10, 3]
skip(5): [10, 7, 9] « pula os 5 primeiros
tudo junto: [10, 9] « as duas maiores notas diferentes
| Operação | Tradução | Recebe | O que faz |
|---|---|---|---|
filter(p) | filtrar | Predicate | Deixa passar só quem dá true |
map(f) | mapear | Function | Troca cada elemento pelo resultado de f (pode mudar o tipo) |
sorted() / sorted(comp) | ordenado | — / Comparator | Ordena (natural ou pelo comparador) |
distinct() | distintos | — | Tira repetidos (usa equals) |
limit(n) | limite | long | Só os n primeiros |
skip(n) | pular | long | Descarta os n primeiros |
peek(c) | espiar | Consumer | Executa algo com cada um sem mudar nada (para depurar) |
mapToInt(f), mapToDouble(f) | ToIntFunction… | Como map, mas gera um IntStream/DoubleStream (para somar, fazer média) | |
flatMap(f) | "achatar" | Function que devolve stream | Cada elemento vira vários (uma lista de listas vira uma lista só) |
Missão: monte um stream que pegue só os nomes com mais de 3 letras, transforme em maiúsculas e ordene. A saída deve ser [BRUNO, CARLA, DUDA].
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> nomes = List.of("ana", "bruno", "carla", "edu", "duda");
List<String> resultado = nomes.stream()
.toList();
System.out.println(resultado);
}
}
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> nomes = List.of("ana", "bruno", "carla", "edu", "duda");
List<String> resultado = nomes.stream()
.filter(n -> n.length() > 3)
.map(n -> n.toUpperCase())
.sorted()
.toList();
System.out.println(resultado);
}
}
Operações terminais: o resultado
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> nomes = List.of("Ana", "Bruno", "Carla", "Beatriz", "Edu");
nomes.stream().filter(n -> n.startsWith("B")).forEach(n -> System.out.println("forEach: " + n));
System.out.println("count(): " + nomes.stream().filter(n -> n.length() > 3).count());
System.out.println("anyMatch(começa com E): " + nomes.stream().anyMatch(n -> n.startsWith("E")));
System.out.println("allMatch(tem 3+ letras): " + nomes.stream().allMatch(n -> n.length() >= 3));
System.out.println("noneMatch(vazio): " + nomes.stream().noneMatch(String::isEmpty));
System.out.println("findFirst(): " + nomes.stream().filter(n -> n.length() > 4).findFirst());
System.out.println("max(por tamanho): " + nomes.stream().max(Comparator.comparing(String::length)).get());
}
}
$ javac Main.java $ java Main forEach: Bruno forEach: Beatriz count(): 3 anyMatch(começa com E): true allMatch(tem 3+ letras): true noneMatch(vazio): true findFirst(): Optional[Bruno] « vem dentro de um Optional max(por tamanho): Beatriz
| Operação | Devolve | Pergunta |
|---|---|---|
toList() | List (imutável) | "Junte tudo numa lista." |
forEach(c) | nada | "Faça isto com cada um." |
count() | long | "Quantos chegaram ao fim?" |
anyMatch(p) | boolean | "Algum satisfaz?" |
allMatch(p) | boolean | "Todos satisfazem?" |
noneMatch(p) | boolean | "Nenhum satisfaz?" |
findFirst() | Optional | "Qual é o primeiro?" |
min(comp) / max(comp) | Optional | "Qual é o menor / maior?" |
reduce(…) | um valor | "Junte todos em um só." |
collect(…) | depende | "Junte do jeito que eu disser" (veja Collectors) |
findFirst, min e max devolvem um Optional ("opcional"): uma caixinha que pode estar vazia, porque o stream pode não ter elemento nenhum. Por isso aparece Optional[Bruno], e por isso o max precisou de .get() para tirar o valor da caixa. O jeito certo de lidar com essa caixa é o assunto da aula Optional.
Números: IntStream, sum e average
Um Stream<Integer> não tem sum(): ele não sabe que os elementos são números. Para fazer contas, converta para um stream de primitivos com mapToInt (ou mapToDouble):
import java.util.List;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
List<Integer> notas = List.of(7, 10, 3, 8);
int soma = notas.stream().mapToInt(n -> n).sum();
System.out.println("sum(): " + soma);
System.out.println("average(): " + notas.stream().mapToInt(n -> n).average());
System.out.println("average().getAsDouble(): " + notas.stream().mapToInt(n -> n).average().getAsDouble());
System.out.println("max(): " + notas.stream().mapToInt(n -> n).max().getAsInt());
System.out.println("vazio average(): " + IntStream.empty().average());
System.out.println("summaryStatistics(): " + notas.stream().mapToInt(n -> n).summaryStatistics());
System.out.println("1 a 100: " + IntStream.rangeClosed(1, 100).sum());
}
}
$ javac Main.java
$ java Main
sum(): 28
average(): OptionalDouble[7.0] « média vem numa caixinha
average().getAsDouble(): 7.0
max(): 10
vazio average(): OptionalDouble.empty « sem elementos, não há média
summaryStatistics(): IntSummaryStatistics{count=4, sum=28, min=3, average=7,000000, max=10}
1 a 100: 5050
- mapToInt(n -> n)
- Transforma cada
Integeremint, gerando umIntStream. Com objetos, você extrai o número:turma.stream().mapToDouble(Aluno::nota). - sum()
- Soma. Com nenhum elemento, dá 0.
- average()
- Média, num
OptionalDouble: vazio se não há elementos (média de nada não existe).getAsDouble()tira o número da caixa. - summaryStatistics()
- Conta, soma, mínimo, média e máximo de uma vez. Repare na média escrita
7,000000, com vírgula: esse texto é formatado com o idioma do computador (pt_BR), como o printf.
reduce: juntar tudo em um valor
reduce ("reduzir") pega os elementos dois a dois e os combina, até sobrar um só. É o que sum, max e count fazem por dentro:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> notas = List.of(7, 10, 3, 8);
int soma = notas.stream().reduce(0, (a, b) -> a + b);
System.out.println("reduce(0, +): " + soma);
System.out.println("reduce(0, Integer::sum): " + notas.stream().reduce(0, Integer::sum));
System.out.println("reduce(1, *): " + notas.stream().reduce(1, (a, b) -> a * b));
System.out.println("reduce(Integer::max): " + notas.stream().reduce(Integer::max));
String frase = List.of("o", "rato", "roeu").stream().reduce("", (a, b) -> a + b + " ");
System.out.println("[" + frase + "]");
}
}
$ javac Main.java $ java Main reduce(0, +): 28 reduce(0, Integer::sum): 28 reduce(1, *): 1680 « 7 × 10 × 3 × 8 reduce(Integer::max): Optional[10] « sem valor inicial: devolve Optional [o rato roeu ]
Como reduce(0, (a, b) -> a + b) funciona com [7, 10, 3, 8]: começa com o valor inicial 0; depois 0 + 7 = 7; 7 + 10 = 17; 17 + 3 = 20; 20 + 8 = 28. O a é o "acumulado até agora" e o b, o próximo elemento. A função é um BinaryOperator (veja Interfaces Funcionais). Sem valor inicial, um stream vazio não teria resultado, por isso volta um Optional.
Na prática, prefira os prontos (sum, max, count) e, para juntar textos, Collectors.joining (veja Collectors), que não deixa o espaço sobrando no fim.
A preguiça: nada roda sem operação terminal
As operações intermediárias são preguiçosas (lazy): elas só montam a linha de montagem. Nenhum elemento passa por ela até que uma operação terminal "ligue a esteira". Veja na prática, com estações que avisam quando trabalham:
import java.util.List;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
List<String> nomes = List.of("Ana", "Bruno", "Carla", "Duda");
System.out.println("1) sem operação terminal:");
Stream<String> s = nomes.stream()
.filter(n -> {
System.out.println(" filter " + n);
return n.length() > 3;
})
.map(n -> {
System.out.println(" map " + n);
return n.toUpperCase();
});
System.out.println(" (nada aconteceu ainda)");
System.out.println("2) com toList():");
List<String> r = s.toList();
System.out.println(" resultado: " + r);
System.out.println("3) com findFirst():");
String primeiro = nomes.stream()
.filter(n -> {
System.out.println(" filter " + n);
return n.length() > 3;
})
.findFirst()
.get();
System.out.println(" primeiro: " + primeiro);
}
}
$ javac Main.java $ java Main 1) sem operação terminal: (nada aconteceu ainda) « filter e map não rodaram! 2) com toList(): filter Ana « Ana não passou no filtro: nem chega ao map filter Bruno map Bruno « Bruno passou: vai direto para o map filter Carla map Carla filter Duda map Duda resultado: [BRUNO, CARLA, DUDA] 3) com findFirst(): filter Ana filter Bruno « achou o primeiro: Carla e Duda nem foram olhadas primeiro: Bruno
Três lições dessa saída real:
- Sem terminal, nada acontece. No passo 1, o programa montou o stream e seguiu em frente: nenhuma lambda foi executada. Um erro comum é escrever
lista.stream().map(…)esperando que algo aconteça, e esquecer a operação terminal. - Um elemento por vez, de cima a baixo. No passo 2, o Bruno passou pelo
filtere logo pelomap, antes de a Carla entrar. Não é "filtra todos, depois mapeia todos": é a esteira. - Operações que param cedo. No passo 3,
findFirstparou no primeiro que serviu. É por isso que dá para usar um stream infinito comoStream.iteratecomlimit: só são gerados os elementos necessários.
Os println dentro do filter acima servem só para enxergar a preguiça. Num programa de verdade, as lambdas de filter e map devem só calcular e devolver, sem efeitos colaterais (sem acrescentar em listas de fora, sem mudar variáveis). Para depurar, existe peek(x -> System.out.println(x)).
Um stream só pode ser usado uma vez
Depois que a esteira roda, ela é desmontada. Guardar um stream numa variável e usá-lo de novo dá erro:
Este código tem um erro de propósito.
import java.util.List;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<String> nomes = List.of("Ana", "Bruno").stream();
System.out.println(nomes.count());
System.out.println(nomes.count());
}
}
$ javac Main.java $ java Main 2 Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:260) at java.base/java.util.stream.ReferencePipeline.count(ReferencePipeline.java:750) at Main.main(Main.java:8)
"Estado ilegal: o stream já foi usado ou fechado." O primeiro count() funcionou; o segundo, na linha 8, não. A solução é pedir um stream novo à fonte a cada uso: lista.stream() de novo. Por isso o costume é não guardar streams em variáveis: guarde a coleção e crie o stream na hora.
Quando dá errado
IllegalStateException: stream has already been operated upon or closed
O que aconteceu: o mesmo objeto Stream recebeu duas operações terminais (ou uma intermediária depois da terminal).
Como consertar: chame colecao.stream() de novo para cada cálculo.
O que aconteceu: faltou a operação terminal: nomes.stream().map(n -> { System.out.println(n); return n; }); não imprime nada.
Como consertar: termine com toList(), forEach, count… Se a intenção era só fazer algo com cada um, use forEach direto na lista.
UnsupportedOperationException ao mexer no resultado do toList()
O que aconteceu: toList() devolve uma lista imutável (como List.of; veja ArrayList).
Como consertar: se precisa alterar depois, use new ArrayList<>(stream.toList()) ou collect(Collectors.toList()) (veja Collectors).
Stream<Integer>
O que aconteceu: só os streams de primitivos (IntStream, DoubleStream, LongStream) têm sum e average.
Como consertar: converta antes: .mapToInt(n -> n).sum().
Referência
| Tipo | Operação | O que faz |
|---|---|---|
| Fonte | c.stream(), Stream.of(…), Arrays.stream(a) | Cria o stream |
| Fonte | IntStream.range(a, b) / rangeClosed(a, b) | Inteiros de a até b−1 / b |
| Intermediária | filter, map, sorted, distinct, limit, skip, peek | Montam a esteira (preguiçosas) |
| Intermediária | mapToInt, mapToDouble, boxed | Para / de streams de primitivos |
| Terminal | toList(), collect(…) | Junta numa coleção |
| Terminal | forEach | Ação para cada um |
| Terminal | count, sum, average, min, max | Números e estatísticas |
| Terminal | anyMatch, allMatch, noneMatch | Perguntas de sim/não |
| Terminal | findFirst, findAny | Um elemento (em Optional) |
| Terminal | reduce(inicial, op) | Combina todos em um |
Exercícios
1. Só os pares
Complete para manter só os números pares:
2. Dobrar
Complete para transformar cada número no seu dobro:
3. Somar
Complete para somar uma List<Integer> notas:
4. O que lista.stream().filter(x -> { System.out.println(x); return true; }); mostra?
5. Qual destas é uma operação terminal?
6. Depois de lista.stream().map(…).toList(), a lista original…
Missão final: calcule, com streams, as estatísticas da turma: a média das notas (mapToDouble + average), quantos ficaram abaixo de 7 (filter + count) e os nomes de quem ficou acima da média, em ordem. A saída deve ser Média: 7.0, Abaixo de 7: 2 e Acima da média: [Ana, Carla].
import java.util.List;
record Aluno(String nome, double nota) { }
public class Main {
public static void main(String[] args) {
List<Aluno> turma = List.of(
new Aluno("Carla", 9.5), new Aluno("Edu", 4.0), new Aluno("Ana", 8.0),
new Aluno("Duda", 6.5), new Aluno("Bruno", 7.0));
double media = 0;
long reprovados = 0;
List<String> destaque = List.of();
System.out.println("Média: " + media);
System.out.println("Abaixo de 7: " + reprovados);
System.out.println("Acima da média: " + destaque);
}
}
import java.util.List;
record Aluno(String nome, double nota) { }
public class Main {
public static void main(String[] args) {
List<Aluno> turma = List.of(
new Aluno("Carla", 9.5), new Aluno("Edu", 4.0), new Aluno("Ana", 8.0),
new Aluno("Duda", 6.5), new Aluno("Bruno", 7.0));
double media = turma.stream().mapToDouble(Aluno::nota).average().getAsDouble();
long reprovados = turma.stream().filter(a -> a.nota() < 7).count();
List<String> destaque = turma.stream()
.filter(a -> a.nota() > media)
.map(Aluno::nome)
.sorted()
.toList();
System.out.println("Média: " + media);
System.out.println("Abaixo de 7: " + reprovados);
System.out.println("Acima da média: " + destaque);
}
}
Resumo
- Um stream é uma linha de montagem: fonte → operações intermediárias → operação terminal.
- Ele não guarda dados nem altera a fonte; o resultado é sempre novo.
- Estações:
filter(descarta),map(transforma),sorted,distinct,limit,skip. - Terminais:
toList,forEach,count,anyMatch/allMatch/noneMatch,findFirst,min/max,reduce. - Contas:
mapToInt/mapToDouble+sum,average(numOptionalDouble). - Preguiça: sem terminal nada roda; os elementos passam um a um;
findFirstelimitparam cedo. - Um stream só pode ser usado uma vez:
IllegalStateException.
Próxima aula: Collectors, para agrupar, contar e juntar os resultados de um stream.