CodeGym /Cursos /JAVA 25 SELF /Spliterator e streams paralelos

Spliterator e streams paralelos

JAVA 25 SELF
Nível 33 , Lição 3
Disponível

1. Introdução ao Spliterator

Se você achava que coleções em Java são percorridas apenas por meio de Iterator, então, até o Java 8, você estava absolutamente certo. Mas com a chegada do Stream API e a moda do paralelismo, surgiu um novo herói — o Spliterator.

Spliterator é uma interface que permite não apenas percorrer os elementos de uma coleção, mas também dividir a fonte de dados em partes para processamento paralelo. O nome é a fusão das palavras split e iterator.

Imagine um bolo grande. Um Iterator comum corta um pedaço e come em ordem. O Spliterator pode cortar o bolo ao meio, dar metade a um amigo — e vocês dois começam a comer ao mesmo tempo. Muitos amigos? Vamos dividir mais!

Interface Spliterator — métodos principais

public interface Spliterator<T> {
    boolean tryAdvance(java.util.function.Consumer<? super T> action);
    Spliterator<T> trySplit();
    long estimateSize();
    int characteristics();
    // ... mais alguns métodos, mas estes são os mais importantes
}
  • tryAdvance — faz algo com o próximo elemento (análogo a next() + ação).
  • trySplit — tenta dividir a fonte em duas partes e retornar um novo Spliterator para a parte “destacada”.
  • estimateSize — estima quantos elementos restam.
  • characteristics — retorna uma máscara de bits das características (ordenação, unicidade, imutabilidade etc.).

2. Uso do Spliterator: iteração manual e divisão

Obtendo um Spliterator a partir de uma coleção

Qualquer coleção que implemente Collection pode fornecer seu Spliterator:

import java.util.List;
import java.util.Spliterator;

List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
Spliterator<String> spliterator = names.spliterator();

Iteração manual de elementos

Spliterator<String> spliterator = names.spliterator();
while (spliterator.tryAdvance(name -> System.out.println("Nome: " + name))) {
    // Tudo é feito dentro de tryAdvance
}

Dividindo a coleção

O mais interessante é o método trySplit():

Spliterator<String> spliterator1 = names.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();

System.out.println("Primeira parte:");
spliterator1.forEachRemaining(System.out::println);

System.out.println("Segunda parte:");
if (spliterator2 != null) {
    spliterator2.forEachRemaining(System.out::println);
}

O que vai acontecer: o Spliterator tentará dividir a coleção em duas partes (nem sempre exatamente ao meio — depende da implementação). Agora você pode processar ambas as partes de forma independente — até mesmo em threads diferentes!

3. Streams paralelos: por que e como isso funciona

Um stream paralelo (parallelStream()) é um stream que processa elementos não em sequência, mas simultaneamente em várias threads. É especialmente útil para grandes volumes de dados e processadores multinúcleo.

import java.util.List;

List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
// Stream sequencial:
names.stream().forEach(System.out::println);
// Stream paralelo:
names.parallelStream().forEach(System.out::println);

Qual é a sacada?
No stream sequencial, os elementos são processados em uma única thread. No paralelo — a fonte é dividida em partes (com o Spliterator), e cada parte é processada em uma thread separada.

Como funciona por dentro?

  1. O Spliterator divide a coleção em partes — geralmente pelo número de núcleos disponíveis (ou um pouco mais).
  2. Cada parte é processada na sua própria thread — utiliza-se o ForkJoinPool comum.
  3. Os resultados são reunidos — combinados em uma coleção final ou valor.

Esquema de funcionamento do stream paralelo

flowchart LR
    A[Coleção] --> B{Spliterator}
    B --> C1[Parte 1] --> D1[Thread 1]
    B --> C2[Parte 2] --> D2[Thread 2]
    B --> C3[Parte 3] --> D3[Thread 3]
    D1 & D2 & D3 --> E[Agregação do resultado]

4. Vantagens e limitações dos streams paralelos

Vantagens

  • Aceleração do processamento de grandes coleções: em cálculos pesados, o stream paralelo acelera perceptivelmente a execução.
  • Simplicidade: não é preciso escrever código multithread manualmente — substitua stream() por parallelStream().

Limitações e armadilhas

  • Nem sempre é mais rápido: para coleções pequenas, a sobrecarga pode “comer” o ganho.
  • A ordem não é garantida: em forEach/map/filter a ordem pode diferir. Se precisar de ordem — use forEachOrdered.
  • Problemas de thread safety: operações com efeitos colaterais (alterar coleções/variáveis externas) levam a condições de corrida.
  • Nem todas as operações são adequadas: cálculos dependentes (por exemplo, acumulação sequencial) podem não funcionar como esperado.

Quando usar streams paralelos?

  • Grandes coleções (dezenas de milhares de elementos ou mais).
  • Operações pesadas em cada elemento.
  • A ordem estrita não é crítica.
  • Sem efeitos colaterais (funções puras).

Quando NÃO usar?

  • Poucos elementos.
  • O código altera variáveis ou coleções externas.
  • É importante preservar a ordem de processamento.
  • A fonte de dados divide-se mal (por exemplo, LinkedList).

5. Exemplos práticos

Exemplo 1: Comparação de tempo de execução

import java.util.*;
import java.util.stream.*;

public class ParallelStreamDemo {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.range(0, 10_000_000)
                                         .boxed()
                                         .collect(Collectors.toList());

        long start = System.currentTimeMillis();
        long count = numbers.stream()
                .filter(n -> isPrime(n))
                .count();
        long time = System.currentTimeMillis() - start;
        System.out.println("Stream sequencial: " + time + " ms, primos encontrados: " + count);

        start = System.currentTimeMillis();
        count = numbers.parallelStream()
                .filter(n -> isPrime(n))
                .count();
        time = System.currentTimeMillis() - start;
        System.out.println("Stream paralelo: " + time + " ms, primos encontrados: " + count);
    }

    // Verificação simples de número primo (para exemplo)
    public static boolean isPrime(int n) {
        if (n < 2) return false;
        for (int i = 2, sqrt = (int)Math.sqrt(n); i <= sqrt; i++)
            if (n % i == 0) return false;
        return true;
    }
}

O que se obtém: em grandes volumes de dados, o stream paralelo geralmente é mais rápido (especialmente em processadores multinúcleo). Em volumes pequenos — pode não haver diferença ou a variante paralela pode ser mais lenta.

Exemplo 2: Problema com a ordem

import java.util.List;

List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
System.out.println("Stream sequencial:");
names.stream().forEach(System.out::println);

System.out.println("Stream paralelo:");
names.parallelStream().forEach(System.out::println);

System.out.println("Stream paralelo com forEachOrdered:");
names.parallelStream().forEachOrdered(System.out::println);

Conclusão: no stream sequencial e ao usar forEachOrdered a ordem é preservada, e no paralelo sem ele — não.

Exemplo 3: Perigo de efeitos colaterais

import java.util.*;
import java.util.stream.*;

List<Integer> numbers = IntStream.range(1, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();

// PERIGOSO! Não faça isso!
numbers.parallelStream().forEach(n -> results.add(n * n));

System.out.println("Tamanho da lista: " + results.size());

O que pode acontecer? O tamanho da lista pode ser menor que o esperado e, às vezes, ocorrerá ConcurrentModificationException. Motivo — ArrayList não é thread-safe, e o stream paralelo dispara várias threads simultaneamente.

6. Spliterator: particularidades e características

Características do Spliterator

O Spliterator descreve suas propriedades por meio de uma máscara de bits:

  • ORDERED — os elementos seguem uma ordem definida (por exemplo, em uma lista).
  • DISTINCT — todos os elementos são únicos (por exemplo, em um conjunto).
  • SORTED — os elementos estão ordenados.
  • SIZED — o tamanho é conhecido.
  • IMMUTABLE — a coleção é imutável.
  • CONCURRENT — a coleção é thread-safe.
  • SUBSIZED — todos os spliterators após trySplit() também conhecem seu tamanho.
Spliterator<String> spliterator = names.spliterator();
int characteristics = spliterator.characteristics();
System.out.println(Integer.toBinaryString(characteristics));

Para que saber isso? O Stream API e os streams paralelos usam esses indicadores para otimizações. Por exemplo, se a fonte é imutável e ordenada, é mais seguro e eficiente dividi-la e agregar o resultado.

7. Quando e como usar Spliterator diretamente?

No dia a dia, raramente é necessário escrever seus próprios Spliterators: as coleções padrão já implementam tudo. Mas se você cria sua própria fonte de dados ou deseja controlar finamente a iteração/divisão, o Spliterator será útil.

Exemplo: iteração manual com tryAdvance

import java.util.List;
import java.util.Spliterator;

List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
Spliterator<String> spliterator = names.spliterator();
spliterator.tryAdvance(name -> System.out.println("Primeiro elemento: " + name));
spliterator.forEachRemaining(name -> System.out.println("Demais: " + name));

Exemplo: divisão da coleção

Spliterator<String> spliterator1 = names.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();

if (spliterator2 != null) {
    spliterator2.forEachRemaining(name -> System.out.println("Parte 2: " + name));
}
spliterator1.forEachRemaining(name -> System.out.println("Parte 1: " + name));

8. Erros típicos ao trabalhar com Spliterator e streams paralelos

Erro nº 1: Usar streams paralelos para coleções pequenas. Em vez de acelerar, você terá uma desaceleração — a sobrecarga de divisão e agendamento de tarefas supera o ganho.

Erro nº 2: Esperar a preservação da ordem dos elementos. Streams paralelos não garantem ordem. Se ela for importante — use forEachOrdered, mas parte da eficiência do paralelismo será perdida.

Erro nº 3: Efeitos colaterais em expressões lambda. Dentro de um stream paralelo não é seguro alterar variáveis/coleções externas — você terá condições de corrida e bugs difíceis de detectar.

Erro nº 4: Uso de coleções não seguras dentro do stream paralelo. Adicionar em um ArrayList comum a partir de várias threads é caminho certo para erros como ConcurrentModificationException.

Erro nº 5: Esperar aceleração instantânea. Streams paralelos não são varinha mágica. Faça profiling: se há poucos dados ou a operação é leve — o stream sequencial é mais rápido.

Erro nº 6: Streams paralelos com fontes que se dividem mal. Por exemplo, LinkedList muitas vezes divide-se de forma ineficiente — o paralelismo pode apenas atrasar a execução.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION