1. Introducción a Spliterator
Si pensabas que las colecciones en Java solo se iteran mediante Iterator, hasta Java 8 estabas absolutamente en lo cierto. Pero con la llegada del Stream API y el auge del paralelismo apareció un nuevo héroe — Spliterator.
Spliterator es una interfaz que permite no solo iterar los elementos de una colección, sino también dividir la fuente de datos en partes para su procesamiento en paralelo. El nombre es la fusión de las palabras split e iterator.
Imagina una gran tarta. Un Iterator normal la corta en trozos y se la come en orden. Spliterator puede cortarla por la mitad, darle una mitad a un amigo — y ambos empezarán a comer a la vez. ¿Muchos amigos? ¡Seguimos dividiendo!
Interfaz Spliterator: métodos principales
public interface Spliterator<T> {
boolean tryAdvance(java.util.function.Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
// ... un par de métodos más, pero estos son los más importantes
}
- tryAdvance — hace algo con el siguiente elemento (análogo a next() + acción).
- trySplit — intenta dividir la fuente en dos partes y devolver un nuevo Spliterator para la parte «desprendida».
- estimateSize — estima cuántos elementos quedan.
- characteristics — devuelve una máscara de bits de características (orden, unicidad, inmutabilidad, etc.).
2. Uso de Spliterator: iteración manual y división
Obtener un Spliterator desde una colección
Cualquier colección que implemente Collection puede proporcionar su Spliterator:
import java.util.List;
import java.util.Spliterator;
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
Spliterator<String> spliterator = names.spliterator();
Iteración manual de elementos
Spliterator<String> spliterator = names.spliterator();
while (spliterator.tryAdvance(name -> System.out.println("Nombre: " + name))) {
// Todo se hace dentro de tryAdvance
}
Dividir una colección
Lo más interesante es el método trySplit():
Spliterator<String> spliterator1 = names.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();
System.out.println("Primera parte:");
spliterator1.forEachRemaining(System.out::println);
System.out.println("Segunda parte:");
if (spliterator2 != null) {
spliterator2.forEachRemaining(System.out::println);
}
¿Qué pasará? Spliterator intentará dividir la colección en dos partes (no siempre exactamente por la mitad — depende de la implementación). Ahora puedes procesar ambas partes de forma independiente — ¡incluso en hilos distintos!
3. Streams paralelos: para qué y cómo funcionan
Un stream paralelo (parallelStream()) es un stream que procesa los elementos no en serie, sino simultáneamente en varios hilos. Es especialmente útil con grandes volúmenes de datos y procesadores multinúcleo.
import java.util.List;
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
// Stream normal:
names.stream().forEach(System.out::println);
// Stream paralelo:
names.parallelStream().forEach(System.out::println);
¿Cuál es la gracia?
En un stream normal, los elementos se procesan en un único hilo. En uno paralelo, la fuente se divide en partes (con Spliterator) y cada parte se procesa en un hilo distinto.
¿Cómo funciona por dentro?
- Spliterator divide la colección en partes — normalmente según el número de núcleos disponibles (o un poco más).
- Cada parte se procesa en su propio hilo — se usa el ForkJoinPool común.
- Los resultados se vuelven a reunir — se combinan en una colección o en un valor final.
Esquema de funcionamiento de un stream paralelo
flowchart LR
A[Colección] --> B{Spliterator}
B --> C1[Parte 1] --> D1[Hilo 1]
B --> C2[Parte 2] --> D2[Hilo 2]
B --> C3[Parte 3] --> D3[Hilo 3]
D1 & D2 & D3 --> E[Agregación del resultado]
4. Ventajas y limitaciones de los streams paralelos
Ventajas
- Aceleración del procesamiento de colecciones grandes: con cálculos pesados, un stream paralelo acelera notablemente la ejecución.
- Simplicidad: no hace falta escribir código multihilo a mano — sustituye stream() por parallelStream().
Limitaciones y escollos
- No siempre más rápido: para colecciones pequeñas, la sobrecarga puede «comerse» la ganancia.
- El orden no está garantizado: en forEach/map/filter el orden puede variar. Si necesitas orden — usa forEachOrdered.
- Problemas de thread safety: las operaciones con efectos secundarios (modificar colecciones/variables externas) provocan condiciones de carrera.
- No todas las operaciones son adecuadas: los cálculos dependientes (por ejemplo, acumulaciones secuenciales) pueden no comportarse como esperas.
¿Cuándo usar streams paralelos?
- Colecciones grandes (decenas de miles de elementos o más).
- Operaciones costosas en cada elemento.
- El orden estricto no es crítico.
- Sin efectos secundarios (funciones puras).
¿Cuándo NO usarlos?
- Pocos elementos.
- El código modifica variables o colecciones externas.
- Es importante preservar el orden de procesamiento.
- La fuente de datos se divide mal (por ejemplo, LinkedList).
5. Ejemplos prácticos
Ejemplo 1: Comparación del tiempo de ejecución
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 normal: " + 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);
}
// Comprobación de número primo muy simple (para el ejemplo)
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;
}
}
¿Qué obtendrás? En grandes volúmenes de datos, el stream paralelo suele ser más rápido (especialmente en procesadores multinúcleo). En volúmenes pequeños puede que no haya diferencia o que la variante paralela resulte más lenta.
Ejemplo 2: Problema con el orden
import java.util.List;
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
System.out.println("Stream normal:");
names.stream().forEach(System.out::println);
System.out.println("Stream paralelo:");
names.parallelStream().forEach(System.out::println);
System.out.println("Stream paralelo con forEachOrdered:");
names.parallelStream().forEachOrdered(System.out::println);
Conclusión: en un stream normal y al usar forEachOrdered se conserva el orden, mientras que en el paralelo sin él — no.
Ejemplo 3: Peligro de efectos secundarios
import java.util.*;
import java.util.stream.*;
List<Integer> numbers = IntStream.range(1, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();
// ¡PELIGROSO! ¡No lo hagas así!
numbers.parallelStream().forEach(n -> results.add(n * n));
System.out.println("Tamaño de la lista: " + results.size());
¿Qué puede pasar? El tamaño de la lista puede ser menor del esperado, y a veces aparecerá ConcurrentModificationException. La razón es que ArrayList no es seguro para hilos, y el stream paralelo lanza varios hilos a la vez.
6. Spliterator: particularidades y características
Características de Spliterator
Spliterator describe sus propiedades mediante una máscara de bits:
- ORDERED — los elementos tienen un orden determinado (por ejemplo, en una lista).
- DISTINCT — todos los elementos son únicos (por ejemplo, en un conjunto).
- SORTED — los elementos están ordenados.
- SIZED — se conoce el tamaño.
- IMMUTABLE — la colección es inmutable.
- CONCURRENT — la colección es segura para hilos.
- SUBSIZED — todos los spliterators después de trySplit() también conocen su tamaño.
Spliterator<String> spliterator = names.spliterator();
int characteristics = spliterator.characteristics();
System.out.println(Integer.toBinaryString(characteristics));
¿Para qué sirve saberlo? El Stream API y los streams paralelos usan estas señales para optimizaciones. Por ejemplo, si la fuente es inmutable y está ordenada, se puede dividir y recopilar el resultado de forma más segura y eficiente.
7. ¿Cuándo y cómo usar Spliterator directamente?
En el día a día rara vez hace falta escribir Spliterators propios: las colecciones estándar ya lo implementan todo. Pero si creas tu propia fuente de datos o quieres controlar con detalle la iteración/división, Spliterator te será útil.
Ejemplo: iteración manual con tryAdvance
import java.util.List;
import java.util.Spliterator;
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
Spliterator<String> spliterator = names.spliterator();
spliterator.tryAdvance(name -> System.out.println("Primer elemento: " + name));
spliterator.forEachRemaining(name -> System.out.println("Los demás: " + name));
Ejemplo: división de una colección
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. Errores típicos al trabajar con Spliterator y streams paralelos
Error n.º 1: Uso de streams paralelos para colecciones pequeñas. En lugar de acelerar, obtendrás una ralentización — la sobrecarga de dividir y planificar tareas superará la ganancia.
Error n.º 2: Esperar que se conserve el orden de los elementos. Los streams paralelos no garantizan el orden. Si es importante — usa forEachOrdered, pero se perderá parte de la eficiencia paralela.
Error n.º 3: Efectos secundarios en expresiones lambda. Dentro de un stream paralelo no es seguro modificar variables/colecciones externas — tendrás condiciones de carrera y errores difíciles de detectar.
Error n.º 4: Uso de colecciones no seguras dentro de un stream paralelo. Añadir a un ArrayList normal desde varios hilos es el camino directo a errores como ConcurrentModificationException.
Error n.º 5: Esperar una aceleración instantánea. Los streams paralelos no son una varita mágica. Perfila: si hay pocos datos o la operación es ligera — el stream normal es más rápido.
Error n.º 6: Streams paralelos con fuentes que se dividen mal. Por ejemplo, LinkedList a menudo se divide de forma ineficiente — el paralelismo puede solo ralentizar la ejecución.
GO TO FULL VERSION