1. Introduzione a Spliterator
Se pensavate che le collection in Java si iterassero solo tramite Iterator, fino a Java 8 avevate perfettamente ragione. Ma con l’arrivo dello Stream API e della moda del parallelismo è arrivato un nuovo eroe — Spliterator.
Spliterator è un’interfaccia che consente non solo di iterare gli elementi di una collection, ma anche di dividere la sorgente di dati in parti per l’elaborazione parallela. Il nome è la fusione di split e iterator.
Immaginate una grande torta. Il normale Iterator la taglia a fette e le mangia in ordine. Spliterator può tagliare la torta a metà, dare una metà a un amico — e mangerete entrambi contemporaneamente. Gli amici sono tanti — dividiamo ancora!
Interfaccia Spliterator — metodi principali
public interface Spliterator<T> {
boolean tryAdvance(java.util.function.Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
// ... ci sono anche altri metodi, ma questi sono i più importanti
}
- tryAdvance — esegue qualcosa con l’elemento successivo (analogo di next() + azione).
- trySplit — prova a dividere la sorgente in due parti e restituisce un nuovo Spliterator per la «parte staccata».
- estimateSize — stima quanti elementi restano.
- characteristics — restituisce una bitmask di caratteristiche (ordinamento, unicità, immutabilità, ecc.).
2. Utilizzo di Spliterator: iterazione manuale e suddivisione
Ottenere uno Spliterator da una collection
Qualsiasi collection che implementi Collection può fornire il proprio Spliterator:
import java.util.List;
import java.util.Spliterator;
List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
Spliterator<String> spliterator = names.spliterator();
Iterazione manuale degli elementi
Spliterator<String> spliterator = names.spliterator();
while (spliterator.tryAdvance(name -> System.out.println("Nome: " + name))) {
// Tutto avviene dentro tryAdvance
}
Suddivisione della collection
La parte più interessante — il metodo trySplit():
Spliterator<String> spliterator1 = names.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();
System.out.println("Prima parte:");
spliterator1.forEachRemaining(System.out::println);
System.out.println("Seconda parte:");
if (spliterator2 != null) {
spliterator2.forEachRemaining(System.out::println);
}
Cosa succede: Spliterator prova a dividere la collection in due parti (non sempre esattamente a metà — dipende dall’implementazione). Ora potete elaborare entrambe le parti in modo indipendente — anche in thread diversi!
3. Stream paralleli: perché e come funzionano
Uno stream parallelo (parallelStream()) è uno stream che elabora gli elementi non in sequenza, ma contemporaneamente in più thread. È particolarmente utile con grandi quantità di dati e processori multi-core.
import java.util.List;
List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
// Stream normale:
names.stream().forEach(System.out::println);
// Stream parallelo:
names.parallelStream().forEach(System.out::println);
Qual è la particolarità?
In uno stream normale gli elementi vengono elaborati in un unico thread. In uno parallelo — la sorgente viene divisa in parti (tramite Spliterator) e ogni parte viene elaborata in un thread separato.
Come funziona internamente?
- Spliterator divide la collection in parti — di solito in base al numero di core disponibili (o poco di più).
- Ogni parte viene elaborata nel proprio thread — si usa il ForkJoinPool condiviso.
- I risultati vengono ricomposti — uniti in una collection o in un valore finale.
Schema di funzionamento dello stream parallelo
flowchart LR
A[Collezione] --> 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[Raccolta del risultato]
4. Vantaggi e limiti degli stream paralleli
Vantaggi
- Accelerazione dell’elaborazione di grandi collection: con calcoli pesanti lo stream parallelo velocizza sensibilmente l’esecuzione.
- Semplicità: non serve scrivere codice multithread a mano — basta sostituire stream() con parallelStream().
Limiti e insidie
- Non sempre più veloce: per collection piccole l’overhead può «mangiare» il beneficio.
- L’ordine non è garantito: in forEach/map/filter l’ordine può differire. Se serve l’ordine — usare forEachOrdered.
- Problemi di thread safety: operazioni con effetti collaterali (modifica di collection/variabili esterne) portano a race condition.
- Non tutte le operazioni sono adatte: calcoli dipendenti (per esempio accumulo strettamente sequenziale) possono non funzionare come previsto.
Quando usare gli stream paralleli?
- Grandi collection (decine di migliaia di elementi o più).
- Operazioni pesanti su ogni elemento.
- L’ordine rigoroso non è critico.
- Nessun effetto collaterale (funzioni pure).
Quando NON usarli?
- Pochi elementi.
- Il codice modifica variabili o collection esterne.
- È importante preservare l’ordine di elaborazione.
- La sorgente dei dati si divide male (ad esempio, LinkedList).
5. Esempi pratici
Esempio 1: Confronto dei tempi di esecuzione
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 normale: " + time + " ms, numeri primi trovati: " + count);
start = System.currentTimeMillis();
count = numbers.parallelStream()
.filter(n -> isPrime(n))
.count();
time = System.currentTimeMillis() - start;
System.out.println("Stream parallelo: " + time + " ms, numeri primi trovati: " + count);
}
// Controllo molto semplice di numero primo (per l'esempio)
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;
}
}
Risultato: su grandi volumi di dati lo stream parallelo è spesso più veloce (soprattutto su processori multi-core). Su insiemi piccoli — la differenza può non esserci o la variante parallela può risultare più lenta.
Esempio 2: Problema con l’ordine
import java.util.List;
List<String> names = List.of("Vasya", "Petya", "Masha", "Lena");
System.out.println("Stream normale:");
names.stream().forEach(System.out::println);
System.out.println("Stream parallelo:");
names.parallelStream().forEach(System.out::println);
System.out.println("Stream parallelo con forEachOrdered:");
names.parallelStream().forEachOrdered(System.out::println);
Conclusione: nello stream normale e con forEachOrdered l’ordine viene preservato, mentre nel parallelo senza di esso — no.
Esempio 3: Pericolo degli effetti collaterali
import java.util.*;
import java.util.stream.*;
List<Integer> numbers = IntStream.range(1, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();
// PERICOLOSO! Non fatelo!
numbers.parallelStream().forEach(n -> results.add(n * n));
System.out.println("Dimensione della lista: " + results.size());
Cosa può succedere? La dimensione della lista può essere inferiore al previsto e talvolta si verificherà una ConcurrentModificationException. Motivo — ArrayList non è thread-safe, mentre lo stream parallelo avvia più thread contemporaneamente.
6. Spliterator: peculiarità e caratteristiche
Caratteristiche di Spliterator
Spliterator descrive le proprie proprietà tramite una bitmask:
- ORDERED — gli elementi seguono un ordine definito (per esempio, in una lista).
- DISTINCT — tutti gli elementi sono unici (per esempio, in un set).
- SORTED — gli elementi sono ordinati.
- SIZED — la dimensione è nota.
- IMMUTABLE — la collection è immutabile.
- CONCURRENT — la collection è thread-safe.
- SUBSIZED — tutti gli spliterator dopo trySplit() conoscono anch’essi la propria dimensione.
Spliterator<String> spliterator = names.spliterator();
int characteristics = spliterator.characteristics();
System.out.println(Integer.toBinaryString(characteristics));
Perché saperlo? Lo Stream API e gli stream paralleli usano questi indicatori per ottimizzazioni. Per esempio, se la sorgente è immutabile e ordinata, può essere divisa e ricomposta in modo più sicuro ed efficiente.
7. Quando e come usare Spliterator direttamente?
Nella vita quotidiana raramente è necessario scrivere Spliterator personalizzati: le collection standard hanno già tutto implementato. Ma se create una vostra sorgente di dati o volete controllare finemente iterazione/suddivisione, Spliterator torna utile.
Esempio: iterazione manuale con 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("Primo elemento: " + name));
spliterator.forEachRemaining(name -> System.out.println("Restanti: " + name));
Esempio: suddivisione della collection
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. Errori tipici nell’uso di Spliterator e degli stream paralleli
Errore n. 1: Utilizzare stream paralleli per collection piccole. Invece di un’accelerazione otterrete un rallentamento — l’overhead di divisione e scheduling supera il beneficio.
Errore n. 2: Aspettarsi la preservazione dell’ordine degli elementi. Gli stream paralleli non garantiscono l’ordine. Se è importante — usate forEachOrdered, ma si perderà parte dell’efficienza del parallelismo.
Errore n. 3: Effetti collaterali nelle espressioni lambda. All’interno di uno stream parallelo non è sicuro modificare variabili/collection esterne — avrete race condition e bug difficili da tracciare.
Errore n. 4: Uso di collection non sicure all’interno di uno stream parallelo. Aggiungere a un normale ArrayList da più thread è la via diretta a errori come ConcurrentModificationException.
Errore n. 5: Aspettarsi un’accelerazione immediata. Gli stream paralleli non sono una bacchetta magica. Profilate: se i dati sono pochi o l’operazione è leggera — lo stream normale è più veloce.
Errore n. 6: Stream paralleli con sorgenti che si dividono male. Per esempio, LinkedList spesso si divide in modo inefficiente — il parallelismo può solo rallentare l’esecuzione.
GO TO FULL VERSION