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.

Você vai aprender
  • 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:

  1. uma fonte: de onde vêm os elementos (uma lista, um array, um intervalo de números);
  2. zero ou mais operações intermediárias: as estações; cada uma devolve um novo stream;
  3. 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 é de String, não mais de Aluno.
.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.

Stream não substitui todo laço

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]
FonteCria
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çãoTraduçãoRecebeO que faz
filter(p)filtrarPredicateDeixa passar só quem dá true
map(f)mapearFunctionTroca cada elemento pelo resultado de f (pode mudar o tipo)
sorted() / sorted(comp)ordenado— / ComparatorOrdena (natural ou pelo comparador)
distinct()distintos—Tira repetidos (usa equals)
limit(n)limitelongSó os n primeiros
skip(n)pularlongDescarta os n primeiros
peek(c)espiarConsumerExecuta 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 streamCada 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çãoDevolvePergunta
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 Integer em int, gerando um IntStream. 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:

  1. 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.
  2. Um elemento por vez, de cima a baixo. No passo 2, o Bruno passou pelo filter e logo pelo map, antes de a Carla entrar. Não é "filtra todos, depois mapeia todos": é a esteira.
  3. Operações que param cedo. No passo 3, findFirst parou no primeiro que serviu. É por isso que dá para usar um stream infinito como Stream.iterate com limit: só são gerados os elementos necessários.
Não use lambdas de stream para mudar coisas de fora

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.

"Não aconteceu nada" (nenhuma saída, nenhum erro)

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).

"cannot find symbol … method sum()" num 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

TipoOperaçãoO que faz
Fontec.stream(), Stream.of(…), Arrays.stream(a)Cria o stream
FonteIntStream.range(a, b) / rangeClosed(a, b)Inteiros de a até b−1 / b
Intermediáriafilter, map, sorted, distinct, limit, skip, peekMontam a esteira (preguiçosas)
IntermediáriamapToInt, mapToDouble, boxedPara / de streams de primitivos
TerminaltoList(), collect(…)Junta numa coleção
TerminalforEachAção para cada um
Terminalcount, sum, average, min, maxNúmeros e estatísticas
TerminalanyMatch, allMatch, noneMatchPerguntas de sim/não
TerminalfindFirst, findAnyUm elemento (em Optional)
Terminalreduce(inicial, op)Combina todos em um

Exercícios

1. Só os pares

Complete para manter só os números pares:

numeros.stream()..toList();

2. Dobrar

Complete para transformar cada número no seu dobro:

numeros.stream()..toList();

3. Somar

Complete para somar uma List<Integer> notas:

int soma = notas.stream()..sum();

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 (num OptionalDouble).
  • Preguiça: sem terminal nada roda; os elementos passam um a um; findFirst e limit param cedo.
  • Um stream só pode ser usado uma vez: IllegalStateException.

Próxima aula: Collectors, para agrupar, contar e juntar os resultados de um stream.