Java – Generazione di numeri “casuali”

Poter generare numeri “casuali” è una necessità che si può avere in svariati tipi di applicazioni, ad esempio in ambito matematico/scientifico ma anche, più banalmente, in applicazioni a scopo ludico o didattico (es. giochi, quiz, ecc...).

Il framework standard di Java fornisce già diverse possibilità per la generazione di numeri casuali. L’obiettivo di questo articolo è di descrivere queste funzionalità con l’aiuto di alcuni esempi pratici.

Innanzitutto, prima di proseguire oltre, è bene precisare che in questi contesti si parla sempre di generazione di numeri pseudo-casuali perché vengono generati da un algoritmo deterministico che, per quanto complesso e sofisticato possa essere, non riuscirà mai a dare veramente il principio di casualità “pura”.

Detto questo, il framework della piattaforma Standard Edition di Java offre principalmente due modi per generare numeri pseudo-casuali:

  • il metodo random della classe java.lang.Math
  • la classe java.util.Random (e la sottoclasse java.security.SecureRandom)

Il metodo random di java.lang.Math

La classe Math mette a disposizione il seguente metodo statico:

public static double random()

Il metodo, essendo static, si invoca sul nome della classe, quindi ad esempio:

double n = Math.random();

La documentazione ufficiale di random chiarisce bene che il valore restituito è un numero casuale «with (approximately) uniform distribution» (con, approssimativamente, distribuzione uniforme) che è maggiore/uguale a 0.0 e minore di 1.0.

In sostanza, 0 è incluso mentre 1 è escluso. Vuol dire che il valore non arriverà mai a 1 ma sarà sempre inferiore, qualcosa al massimo tipo 0,99999…. Se si vuole fare un piccolo “esperimento”, si può provare un breve sorgente per verificare il valore massimo ottenibile dopo ad esempio 100000 iterazioni.

public class ProvaMaxRandom {
    public static void main(String[] args) {
        double massimo = 0;

        for (int i = 0; i < 100000; i++) {
            massimo = Math.max(massimo, Math.random());
        }

        System.out.println("massimo = " + massimo);
    }
}

In output si può ottenere un risultato come ad esempio (varia da prova a prova):

massimo = 0.999998469849328

Il valore massimo quindi si avvicina moltissimo a 1 ma non sarà mai esattamente uguale a 1. Questo è sempre da considerare e ricordare quando si usa il metodo random di Math.

Chiaramente un intervallo di valori così ridotto è raramente utile. Se si vuole ottenere un qualunque altro intervallo e soprattutto se si vogliono avere numeri “interi” (ad esempio come tipo int), è sufficiente mettere insieme varie altre operazioni tra cui una eventuale combinazione di: a) una moltiplicazione, b) una addizione o sottrazione, c) un operatore di cast esplicito (es. al tipo int).

Di seguito vengono presentati alcuni esempi d’uso:

Valori double tra 0 (incluso) e 100 (escluso!)

double n = Math.random() * 100;

Questo caso è semplicissimo, è sufficiente moltiplicare per 100 per ottenere un intervallo più ampio. Il valore massimo sarà qualcosa del tipo 99,99999… ma non arriverà mai esattamente a 100.

Valori int tra 0 e 50 (entrambi inclusi)

int n = (int) (Math.random() * 51);

In questo caso si moltiplica per 51, così il valore massimo sarà qualcosa del tipo 50,99999… e il cast a int di fatto tronca tutti i decimali, ottenendo quindi un intervallo di valori interi tra 0 e 50 inclusi.

Attenzione alle parentesi attorno alla moltiplicazione (Math.random() * 51) perché non sono facoltative! Omettere queste parentesi è un “classico” errore. L’operatore di cast, per sua definizione in Java, ha precedenza rispetto ad una moltiplicazione. Se non ci fossero quelle parentesi, prima farebbe il cast (risultato sempre 0, dal momento che tronca i decimali) e poi dopo la moltiplicazione per 51 (ovviamente, risultato ancora sempre 0).

Valori int tra -20 e 20 (entrambi inclusi)

int n = (int) (Math.random() * 41) - 20;

In questo caso si moltiplica per 41 così, grazie al cast (che avviene prima della sottrazione), si ha un intervallo di valori interi tra 0 e 40 inclusi. La sottrazione semplicemente sposta l’intervallo ottenendo un nuovo intervallo tra -20 e 20 inclusi.


Per qualunque altro intervallo è sufficiente ragionare in maniera similare a quanto fatto negli esempi riportati sopra.

Il random di Math è un metodo estremamente basilare per generare numeri casuali e va benissimo negli scenari più semplici, quando si devono generare uno o pochi valori e magari solo ogni tanto. Se si vuole fare qualcosa di più, è necessario ricorrere alla classe java.util.Random descritta di seguito.

La classe java.util.Random

La classe Random offre maggiore flessibilità rispetto al metodo random di Math. Innanzitutto Random è una classe “concreta” e quindi deve essere istanziata per poter ottenere un oggetto su cui invocare i suoi metodi. Esistono solo due costruttori:

  • public Random()
  • public Random(long seed)

Random per poter funzionare necessita di un “seme” (seed in inglese), è un valore numerico che serve per inizializzare l’algoritmo di generazione dei numeri pseudo-casuali. Il concetto importante da sapere è che a parità di seme si riottiene sempre la stessa sequenza di numeri.

Con il primo costruttore indicato il seme è scelto casualmente (basandosi principalmente anche sull’istante di tempo corrente). Mentre con il secondo costruttore il seme deve essere passato esplicitamente. Generalmente è sufficiente usare il primo costruttore, quello senza argomenti.

Una volta ottenuto un oggetto Random si possono usare tutti i suoi metodi. Random offre svariati metodi del tipo (ne riporto solo alcuni per brevità):

  • public boolean nextBoolean()
  • public int nextInt()
  • public int nextInt(int bound)
  • public float nextFloat()
    ecc...

Avendo a disposizione un oggetto Random si possono invocare tutti questi metodi quante volte si vuole. Detto in altro modo, NON c’è bisogno di creare un nuovo oggetto Random ad ogni generazione di un valore. Sarebbe inutile e anche controproducente.

Uno dei metodi tipicamente più utili è il nextInt(int bound). La documentazione ufficiale chiarisce bene che il valore generato è compreso tra 0 incluso e il valore bound specificato escluso. Anche in questo caso il limite superiore è escluso. Nota: il valore di bound deve essere positivo, altrimenti viene lanciata la eccezione IllegalArgumentException.

Di seguito vengono presentati alcuni esempi d’uso:

Valori int tra 0 e 40 (entrambi inclusi)

Random rnd = new Random();
int n = rnd.nextInt(41);

In questo caso i valori ottenibili sono in un intervallo tra 0 e 40 inclusi.

Valori int tra -100 e 100 (entrambi inclusi)

Random rnd = new Random();
int n = rnd.nextInt(201) - 100;

In questo caso i valori ottenibili dal nextInt(201) sono in un intervallo tra 0 e 200 inclusi. La sottrazione semplicemente sposta l’intervallo ottenendo un nuovo intervallo tra -100 e 100 inclusi.


Per qualunque altro intervallo è sufficiente ragionare in maniera similare a quanto fatto negli esempi riportati sopra.

La classe java.security.SecureRandom

Esiste una ulteriore classe nel framework, si chiama SecureRandom ed estende la classe Random. Come si può dedurre dal nome del package, essa fa parte della “security” di Java. SecureRandom infatti è un generatore di numeri casuali crittograficamente “forte” ed è utilizzato tipicamente in ambito crittografico.

Un generatore di numeri casuali adatto all’uso in campo crittografico deve avere delle proprietà molto ben specifiche, come ad esempio il fatto che se si riesce ad osservare una parte della sequenza, non deve essere possibile ricostruire l’intera sequenza.

Tecnicamente sarebbe perfettamente possibile utilizzare SecureRandom in contesti dove Random sarebbe già appropriato (come detto, SecureRandom è una sottoclasse di Random). Il contrario ovviamente no, soprattutto per ragioni concettuali. Random NON deve essere usato in ambito crittografico, in quanto non possiede quelle proprietà che rendono un generatore di numeri casuali crittograficamente “forte”.

È bene comunque precisare che la computazione dei numeri in SecureRandom è più onerosa rispetto a Random e quindi SecureRandom si usa principalmente in quei contesti specifici legati alla crittografia dove quel costo computazionale ha un senso e porta dei benefici.

Conclusioni

Con questo articolo ho descritto come poter generare numeri (“pseudo”) casuali usando il framework standard di Java. Gli esempi che ho scelto, anche se molto brevi, dovrebbero essere abbastanza significativi per far capire come ragionare in generale per qualunque altro caso o necessità.

Quello che ho appena descritto potrà sicuramente darmi lo spunto per realizzare ulteriori articoli che spiegano come sfruttare la generazione di numeri casuali in scenari più pratici e realistici, ad esempio per mescolare casualmente un array o per estrarre valori casuali “unici” (come il classico caso dei numeri nel gioco della Tombola).

Riferimenti