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?
- O Spliterator divide a coleção em partes — geralmente pelo número de núcleos disponíveis (ou um pouco mais).
- Cada parte é processada na sua própria thread — utiliza-se o ForkJoinPool comum.
- 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.
GO TO FULL VERSION