CodeGym /Corsi /JAVA 25 SELF /Spliterator e stream paralleli

Spliterator e stream paralleli

JAVA 25 SELF
Livello 33 , Lezione 3
Disponibile

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?

  1. Spliterator divide la collection in parti — di solito in base al numero di core disponibili (o poco di più).
  2. Ogni parte viene elaborata nel proprio thread — si usa il ForkJoinPool condiviso.
  3. 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.

1
Compito
JAVA 25 SELF, livello 33, lezione 3
Bloccato
Elaborazione del primo pacco arrivato in magazzino 📦
Elaborazione del primo pacco arrivato in magazzino 📦
1
Compito
JAVA 25 SELF, livello 33, lezione 3
Bloccato
Divisione dei compiti tra i team alla riunione strategica 🎯
Divisione dei compiti tra i team alla riunione strategica 🎯
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION