Introduzione agli Spliterator in Java

Concetti Chiave
  • Spliterator è un tipo di iteratore introdotto in JDK 8 che supporta l'iterazione parallela.
  • Fornisce metodi per iterare su sequenze di elementi, combinando le operazioni hasNext e next in un unico metodo.
  • Utilizza l'interfaccia Consumer per applicare azioni agli elementi durante l'iterazione.
  • Le caratteristiche di Spliterator possono essere utilizzate per ottimizzare l'iterazione e la programmazione parallela.
  • Esistono sotto-interfacce per tipi primitivi come Spliterator.OfDouble, Spliterator.OfInt, e Spliterator.OfLong.

Spliterator

JDK 8 ha aggiunto un altro tipo di iteratore chiamato spliterator che è definito dall'interfaccia Spliterator.

Uno spliterator percorre una sequenza di elementi, e in questo senso, è simile agli iteratori descritti nella lezione precedente. Tuttavia, le tecniche necessarie per utilizzarlo differiscono.

Inoltre, offre sostanzialmente più funzionalità rispetto a Iterator o ListIterator. Forse l'aspetto più importante di Spliterator è la sua capacità di fornire supporto per l'iterazione parallela di porzioni della sequenza. Quindi, Spliterator supporta la programmazione parallela. Studieremo in dettaglio la programmazione parallela e concorrente nelle prossime lezioni.

Tuttavia, è possibile utilizzare Spliterator anche se non si utilizzerà l'esecuzione parallela. Un motivo per cui si potrebbe volerlo fare è perché offre un approccio semplificato che combina le operazioni hasNext e next in un unico metodo.

Spliterator è un'interfaccia generica che è dichiarata così:

interface Spliterator<T>

Qui, T è il tipo di elementi su cui si itera. Spliterator dichiara i metodi mostrati nella tabella che segue:

Metodo Descrizione
int characteristics() Restituisce le caratteristiche dello spliterator invocante codificate in un intero.
long estimateSize() Stima il numero di elementi rimasti da iterare e restituisce il risultato. Restituisce Long.MAX_VALUE se il conteggio non può essere ottenuto per qualsiasi motivo.
default void forEachRemaining(Consumer<? super T> azione) Applica azione a ogni elemento non processato nella sorgente dati.
default Comparator<? super T> getComparator() Restituisce il comparatore utilizzato dallo spliterator invocante o null se viene utilizzato l'ordinamento naturale. Se la sequenza è disordinata, viene lanciata IllegalStateException.
default long getExactSizeIfKnown() Se lo spliterator invocante è dimensionato, restituisce il numero di elementi rimasti da iterare. Restituisce –1 altrimenti.
default boolean hasCharacteristics(int val) Restituisce true se lo spliterator invocante ha le caratteristiche passate in val. Restituisce false altrimenti.
boolean tryAdvance(Consumer<? super T> azione) Esegue azione sull'elemento successivo nell'iterazione. Restituisce true se c'è un elemento successivo. Restituisce false se non rimangono elementi.
Spliterator<T> trySplit() Se possibile, divide lo spliterator invocante, restituendo un riferimento a un nuovo spliterator per la partizione. Altrimenti, restituisce null. Quindi, se ha successo, lo spliterator originale itera su una porzione della sequenza e lo spliterator restituito itera sull'altra porzione.
Tabella 1: I Metodi Dichiarati da Spliterator

Utilizzare Spliterator per compiti di iterazione di base è abbastanza facile: basta chiamare tryAdvance() finché non restituisce false. Se applichiamo la stessa azione a ogni elemento nella sequenza, forEachRemaining() offre un'alternativa semplificata. In entrambi i casi, l'azione che si verificherà con ogni iterazione è definita da ciò che l'oggetto Consumer fa con ogni elemento. Consumer è un'interfaccia funzionale che applica un'azione a un oggetto. È un'interfaccia funzionale generica dichiarata in java.util.function. Consumer specifica solo un metodo astratto, accept(), che è mostrato qui:

void accept(T objRef)

Nel caso di tryAdvance(), ogni iterazione passa l'elemento successivo nella sequenza a objRef. Spesso, il modo più facile per implementare Consumer è tramite l'uso di un'espressione lambda.

Il seguente programma fornisce un semplice esempio di Spliterator. Notate che il programma dimostra sia tryAdvance() che forEachRemaining(). Si noti anche come questi metodi combinano le azioni dei metodi next() e hasNext() di Iterator in una singola chiamata.

// Una semplice dimostrazione di Spliterator.
import java.util.*;

class DemoSpliterator {

    public static void main(String[] args) {
        // Crea un array list per double.
        ArrayList<Double> valori = new ArrayList<>();

        // Aggiungi valori all'array list.
        valori.add(1.0);
        valori.add(2.0);
        valori.add(3.0);
        valori.add(4.0);
        valori.add(5.0);

        // Usa tryAdvance() per visualizzare i contenuti di valori.
        System.out.print("Contenuti di valori:\n");
        Spliterator<Double> spltitr = valori.spliterator();
        while (spltitr.tryAdvance((n) -> System.out.println(n)))
            ;
        System.out.println();

        // Crea una nuova lista che contiene radici quadrate.
        spltitr = valori.spliterator();
        ArrayList<Double> radiciQuadrate = new ArrayList<>();
        while (spltitr.tryAdvance((n) -> radiciQuadrate.add(Math.sqrt(n))))
            ;

        // Usa forEachRemaining() per visualizzare
        // i contenuti di radiciQuadrate.
        System.out.print("Contenuti di radiciQuadrate:\n");
        spltitr = radiciQuadrate.spliterator();
        spltitr.forEachRemaining((n) -> System.out.println(n));
        System.out.println();
    }

}

L'output è mostrato qui:

Contenuti di valori:
1.0
2.0
3.0
4.0
5.0

Contenuti di radiciQuadrate:
1.0
1.4142135623730951
1.7320508075688772
2.0
2.23606797749979

Sebbene questo programma dimostri i meccanismi dell'utilizzo di Spliterator, non rivela il suo pieno potere. Come menzionato, il massimo beneficio di Spliterator si trova in situazioni che coinvolgono l'elaborazione parallela.

Nella tabella di sopra, si notino i metodi characteristics() e hasCharacteristics().

Ogni Spliterator ha un insieme di attributi, chiamati caratteristiche, associati ad esso. Questi sono definiti da campi int statici in Spliterator, come SORTED, DISTINCT, SIZED, e IMMUTABLE, per citarne alcuni. È possibile ottenere le caratteristiche chiamando characteristics(). È possibile determinare se una caratteristica è presente chiamando hasCharacteristics(). Spesso, non si ha bisogno di accedere alle caratteristiche di uno Spliterator, ma in alcuni casi, possono aiutare a creare codice efficiente e resiliente.

Ritorneremo sugli Spliterator nelle prossime lezioni.

Ci sono diverse sotto-interfacce annidate di Spliterator progettate per l'uso con i tipi primitivi double, int, e long. Queste sono chiamate Spliterator.OfDouble, Spliterator.OfInt, e Spliterator.OfLong. C'è anche una versione generalizzata chiamata Spliterator.OfPrimitive(), che offre flessibilità aggiuntiva e serve come super-interfaccia delle suddette.