Java – Creazione di uno Stream di dati “casuali”

La Stream API, introdotta nel JDK 8, viene generalmente e tipicamente utilizzata per operare sui dati che provengono dalle “collezioni” (liste, set, ecc...) oppure, in certi casi, dagli array. Ma cosa si può fare se si volesse creare uno stream di dati “casuali”? Mi è capitato pochi giorni fa di pensarci, le soluzioni effettivamente esistono e sono semplici ma bisogna prima stabilire se si intende ottenere uno stream di numeri (int, long, ecc...) casuali oppure uno stream di oggetti casuali.

Stream di numeri casuali

Se l’obiettivo è quello di creare uno stream di numeri casuali, la “bella notizia” è che il framework standard di Java offre già questa funzionalità senza dover fare nulla di particolare. La classe java.util.Random (insieme alle sottoclassi SecureRandom e ThreadLocalRandom) nel JDK 8 è stata aggiornata ed espansa per offrire una serie di metodi in grado di creare degli stream di numeri casuali.

In particolare Random permette di creare degli stream di numeri int, long e double restituendo rispettivamente un IntStream, LongStream e DoubleStream. Per ciascuno di questi tipi vengono offerte ben 4 versioni di metodi (nota: in overloading poiché hanno lo stesso nome) per creare le varie combinazioni tra: con/senza lunghezza esplicita e con/senza intervallo di valori esplicito.

I nuovi metodi di Random sono i seguenti:

public IntStream ints()
public IntStream ints(int randomNumberOrigin, int randomNumberBound)
public IntStream ints(long streamSize)
public IntStream ints(long streamSize, int randomNumberOrigin, int randomNumberBound)
public LongStream longs()
public LongStream longs(long randomNumberOrigin, long randomNumberBound)
public LongStream longs(long streamSize)
public LongStream longs(long streamSize, long randomNumberOrigin, long randomNumberBound)
public DoubleStream doubles()
public DoubleStream doubles(double randomNumberOrigin, double randomNumberBound)
public DoubleStream doubles(long streamSize)
public DoubleStream doubles(long streamSize, double randomNumberOrigin, double randomNumberBound)

Su tutti questi metodi ci sono alcune cose da osservare:

  • streamSize serve per specificare e fissare la lunghezza dello stream. Nelle versioni dove non c'è streamSize, lo stream di numeri è di per sé “infinito” e lo si può eventualmente limitare successivamente usando ad esempio l’operazione di limit(long maxSize) degli stream.
  • randomNumberOrigin e randomNumberBound servono per specificare un intervallo di valori da utilizzare. Da notare che randomNumberOrigin è inclusivo mentre randomNumberBound è esclusivo (ovvero, l’intervallo non comprende questo valore). Nelle versioni dove non ci sono questi due parametri, l’intervallo di valori è quello massimo possibile per quel tipo di dato.

Generare uno stream di numeri casuali quindi è molto semplice. Il seguente è un esempio di generazione di 100 numeri casuali compresi tra -20 e 20 (entrambi inclusi):

import java.util.Random;
import java.util.stream.IntStream;

public class NumeriRandom {
    public static void main(String[] args) {
        Random rnd = new Random();
        IntStream stream = rnd.ints(100, -20, 21);     // 21 NON è incluso!

        stream.forEach(n -> System.out.print(n + " "));
    }
}

Esempio di output:

-2 6 -7 9 14 -9 8 20 -1 -10 -14 -2 12 -7 18 16 5 -13 -17 -18 1 -14 -11 -4 1 -15 15 14 -18 17 -13 3 -17 19 18 -7 0 -6 1 -20 -20 20 15 0 -18 -16 -9 -12 7 3 2 5 2 14 -14 -8 9 -11 -19 -12 20 16 2 -9 5 18 0 10 14 -13 -5 17 -15 8 -16 -16 4 18 -8 -11 -17 -2 -7 13 -9 -12 -18 10 -12 2 -3 -9 -9 14 -13 7 -19 -16 19 -10 

Stream di oggetti casuali

E se invece si volesse generare uno stream di oggetti casuali? Prima di continuare è meglio chiarire l’obiettivo, prendiamo come esempio un semplice array che contiene dei nomi di colori.

String[] colori = { "rosso", "giallo", "verde", "blu" };

Dato questo array, si vuole creare un Stream<String> che sia in grado fornire un certo numero di stringhe (per esempio 30) prendendole “a caso” dall’array colori. Una funzionalità di questo tipo purtroppo non è presente nella classe Random (e nelle sue sottoclassi) e nemmeno nel resto del framework standard di Java.

Per curiosità, ho provato a verificare se una funzionalità del genere fosse disponibile in alcune delle più note librerie di “utilità” come ad esempio la Apache Commons Lang oppure la Google Guava. Purtroppo mi risulta che neanche in queste librerie esiste una funzionalità come quella descritta.

Tuttavia, è abbastanza facile realizzare questa funzionalità e richiede davvero poco codice! È sufficiente sfruttare il concetto di “mappatura” (l’operazione di map degli stream) partendo da uno stream che genera degli indici “casuali” da usare all’interno dell’array. E siccome si sta parlando di uno stream di oggetti che potrebbero essere di qualunque tipo, non solo String, può essere molto utile mettere in gioco anche la funzionalità dei generics per creare uno stream in maniera più generalizzata.

Il codice di esempio è il seguente:

import java.util.Random;
import java.util.stream.Stream;

public class OggettiRandom {
    public static void main(String[] args) {
        String[] colori = { "rosso", "giallo", "verde", "blu" };

        Random rnd = new Random();
        Stream<String> stream = randomObjects(rnd, 30, colori);

        stream.forEach(s -> System.out.print(s + " "));
    }

    public static <T> Stream<T> randomObjects(Random rnd, long streamSize, T[] objects) {
        return rnd.ints(streamSize, 0, objects.length).mapToObj(i -> objects[i]);
    }
}

Esempio di output:

blu verde rosso rosso rosso blu giallo giallo blu blu giallo blu rosso verde giallo rosso rosso verde blu rosso rosso blu blu rosso verde verde blu rosso blu giallo 

Nel codice di esempio, randomObjects è un metodo “generico” (nel senso dei generics) poiché introduce la dichiarazione della type variable T. La parametrizzazione dello Stream corrisponde al tipo di elementi dell’array e quindi risulta tutto completamente type safe. Se si passa un array String[] si ottiene un Stream<String>, se si passa un array BigInteger[] si ottiene un Stream<BigInteger> e così via.

Il primo passo è quello di creare un IntStream che possa fornire dei valori casuali compresi tra 0 e lunghezza_array-1, mentre il secondo passo consiste semplicemente in una “mappatura” (metodo mapToObj) per trasformare un indice nell’oggetto contenuto a quell’indice.

Naturalmente, si possono realizzare tutte le “varianti” che si vogliono, come ad esempio un’altra versione di randomObjects che non ha il parametro streamSize, oppure una versione come metodo varargs, cioè con un numero variabile di argomenti, in modo da ricevere direttamente gli N elementi. Quest’ultimo caso è anche interessante/particolare e merita un breve approfondimento.

L’esempio precedente si può trasformare nel seguente:

import java.util.Random;
import java.util.stream.Stream;

public class OggettiRandom2 {
    public static void main(String[] args) {
        Random rnd = new Random();
        Stream<String> stream = randomItems(rnd, 30, "rosso", "giallo", "verde", "blu");

        stream.forEach(s -> System.out.print(s + " "));
    }

    @SafeVarargs
    public static <T> Stream<T> randomItems(Random rnd, long streamSize, T... items) {
        return rnd.ints(streamSize, 0, items.length).mapToObj(i -> items[i]);
    }
}

In questo scenario, i concetti e l’approccio generale sono praticamente gli stessi del precedente esempio. Il metodo randomItems materialmente riceve un array, quindi si comporta esattamente come il metodo randomObjects. L’unica vera differenza è che randomItems è un metodo detto varargs, cioè può ricevere un numero variabile di argomenti.

Sfortunatamente, gli array e i generics non vanno molto d’accordo per via del meccanismo della erasure, che è la tecnica utilizzata per implementare i generics. Un parametro vararg che usa come tipo una type variable è potenzialmente un problema per la type safety generale del programma. Se non si applica al metodo l’annotazione @SafeVarargs, il compilatore emette un warning:

warning: [unchecked] Possible heap pollution from parameterized vararg type T

Il problema può esistere realmente se il metodo scrivesse all’interno dell’array in maniera non appropriata oppure se il reference all’array creato dal compilatore venisse “esposto” in qualche modo, restituendolo o memorizzandolo da qualche parte. Fortunatamente, il metodo randomItems non fa nulla di tutto questo e invece si limita solamente a leggere i valori contenuti nell’array, quindi di fatto è “innocuo”. L’applicazione della annotazione @SafeVarargs serve solo ad assicurare al compilatore che sappiamo quello che stiamo facendo (il warning non viene più emesso), ben sapendo che il metodo è innocuo e non può causare alcun problema con gli elementi dell’array.