Kirjautuminen

Haku

Tehtävät

Koodit: Java: Satunnaislukujen arpominen

Kirjoittaja: Metabolix

Kirjoitettu: 22.01.2013 – 16.03.2013

Tagit: tietoturva, koodi näytille, vinkki

Kun ohjelmoinnissa puhutaan satunnaisluvuista, tarkoitetaan yleensä pseudosatunnaislukuja eli lukuja, jotka näyttävät satunnaisilta mutta perustuvat johonkin kaavaan. Näistä tämäkin vinkki kertoo. Aidot satunnaisluvut ovat ohjelmoinnissa harvinaisempia, koska niitä on vaikeampi tuottaa tietokoneella eikä niitä todella tarvita kuin joissain erityistilanteissa.

Random-luokka

Lukuja voi Javassa helpoiten arpoa luokalla java.util.Random.

import java.util.Random;

public class Esimerkki {
	public static Random random = new Random();

	public static void main(String[] args) {
		System.out.println("Lukuja:");
		System.out.println(random.nextInt());      // int
		System.out.println(random.nextInt(10));    // int, 0 <= x < 10, eli maksimi on 9.
		System.out.println(random.nextLong());     // long
		System.out.println(random.nextDouble());   // double, 0.0 <= x < 1.0
		System.out.println(random.nextGaussian()); // double, normaalijakauma
	}
}

Siemenluku

Random-olion tuottamat luvut riippuvat siemenluvusta. Tavallisesti Java valitsee siemenluvun satunnaisesti niin, että käytännössä kaikki Random-oliot saavat eri siemenluvun.

Joskus on hyödyllistä tuottaa toistuvasti samat luvut. Silloin Random-oliolle voi itse asettaa siemenluvun joko olion luonnin yhteydessä tai erikseen setSeed-metodilla.

import java.util.Random;

public class Esimerkki {
	public static Random random = new Random(1234); // Aina samat luvut; siemen = 1234

	public static void main(String[] args) {
		System.out.println("Luvut: " + random.nextInt(100) + ", " + random.nextInt(100));

		// Uudestaan samat:
		random.setSeed(1234);
		System.out.println("Luvut: " + random.nextInt(100) + ", " + random.nextInt(100));
	}
}

Kokonaisluku tietyltä väliltä

Kokonaisluvut välillä 3–7 ovat 3, 4, 5, 6 ja 7. Lukuja on yhteensä 5, ja pienin niistä on 3. Luvun tästä joukosta saa arvottua niin, että arpoo ensin luvun väliltä 0–4 (nextInt(5)) ja lisää tulokseen 3. Menetelmän voi yleistää metodiksi näin:

public static int nextInt(Random random, int min, int max) {
	return min + random.nextInt(max - min + 1);
}
// System.out.println(nextInt(random, 3, 7));

Metodi vaatii, että välin päät annetaan oikeassa järjestyksessä ja että lukuväli on tarpeeksi pieni, enintään noin 2 miljoonaa.

Liukuluku tietyltä väliltä

Lukuvälin 0–1, jolta nextDouble arpoo lukuja, saa muunnettua lukuväliksi 3–7 jälleen kahdessa vaiheessa: ensin skaalataan se kertolaskulla väliksi 0–4 ja sitten siirretään yhteenlaskulla väliksi 3–7. Metodi on yhtä lyhyt kuin edellinenkin:

public static double nextDouble(Random random, double min, double max) {
	return min + (max - min) * random.nextDouble();
}
// System.out.println(nextDouble(random, 3.0, 7.0)); // 3.0 <= x < 7.0.

Tämäkin metodi luonnollisesti vaatii, että annettu lukualue (max - min) voidaan ilmoittaa double-tyyppisenä arvona.

Äsken kokonaislukujen yhteydessä lukuväliin 3–7 kuului myös luku 7 eli väli oli suljettu. Tällä kertaa näin ei ole, vaan lukuvälin suurin luku on 6,999... sillä tarkkuudella, kuin tietokone pystyy sen esittämään, eli väli on avoin. Toisaalta myös tietokoneen pyöristysvirheet voivat vaikuttaa lukuväliin, jos päätepisteet ovat hyvin eri suuruusluokkaa.

SecureRandom-luokka

Javassa on myös toinen satunnaislukuja tuottava luokka, java.security.SecureRandom. Viihdekäytössä tästä luokasta ei ole hyötyä, mutta jos satunnaislukuja käytetään johonkin tietoturva-asiaan, luokka on paikallaan. Luokka toimii käytännössä samoin kuin Random-luokka, mutta se luodaan eri tavalla:

import java.util.Random;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;

public class Esimerkki {
	public static void main(String[] args) {
		Random random;
		try {
			random = SecureRandom.getInstance("SHA1PRNG");
		} catch (NoSuchAlgorithmException e) {
			System.err.println("Pyydettyä algoritmia ei löydy!");
			System.exit(1);
			return;
		}
		System.out.println(random.nextInt());
	}
}

Tässä SHA1PRNG on lukujen generointiin käytettävän algoritmin tunnus. Tämä on vain eräs mahdollisista algoritemista, ja Javan eri versiot voivat sisältää eri algoritmeja. Niiden tarkempi käsittely ei kuulu tähän vinkkiin.

Kommentit

User137 [23.01.2013 15:08:30]

#

Siemenluvun valinta esimerkiksi tietokoneen kellon avulla on tarpeen, jos et halua lukujen menevän aina samalla tavalla. Oletan että näin halutaan ohjelman toimivan 99% tapauksista. Tähän on ainakin 2 funktiota:

Random random = new Random(System.currentTimeMillis());
tai
Random random = new Random(System.nanoTime());

Millisekunteina tai nanosekunteina. Tavallisesti millisekunnit riittää, mutta jos sama ohjelma pyöräytetään käyntiin (toisen prosessin toimesta) useita kertoja saman millisekunnin aikana, voi nanosekunnit olla tarpeen. Toisaalta kaikki järjestelmät/laitteet eivät välttämättä tue nanosekunteja.

Metabolix [23.01.2013 19:55:19]

#

User137, luitko vinkin? Siinä sanotaan, että Java tekee tuon automaattisesti.

Omalla koneellani ei tullut kahta samanlaista Random-oliota, vaikka laitoin ohjelmaan kaksi new-riviä peräkkäin. Näyttää siis, että Javan valmis menetelmä on varsin riittävä.

Aika monessa käytössä ei edes ole merkitystä, ovatko luvut eri kerroilla erilaiset, eikä yleensä varsinkaan sellaisissa ohjelmissa, joita joku voisi käynnistää skriptillä monta kerralla.

Sami [24.01.2013 00:19:37]

#

Koodivinkissä on muuten pieni kirjoitusvirhe: "Välillä 3–7 eli joukossa 3, 4, 5, 6, 7 on lukuja yhteensä 5, ja pienin luku on 0."

Ja syy, ettet saanut kahta samaa Random-oliota (tai vielä oikeammin samalla siemenluvulla alustettua oliota) johtuu todennäköisesti siitä kuinka Random-luokka on toteutettu.

Esimerkiksi Oraclen toteutus asiasta käyttää kelloa ja pseudosatunnaislukua siemenluvun valitsemiseen, jolloin tuloksena on lähes aina eri siemenluku:

/**
 * Creates a new random number generator. This constructor sets
 * the seed of the random number generator to a value very likely
 * to be distinct from any other invocation of this constructor.
 */
public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}

private static long seedUniquifier() {
    // L'Ecuyer, "Tables of Linear Congruential Generators of
    // Different Sizes and Good Lattice Structure", 1999
    for (;;) {
        long current = seedUniquifier.get();
        long next = current * 181783497276652981L;
        if (seedUniquifier.compareAndSet(current, next))
            return next;
    }
}

private static final AtomicLong seedUniquifier
    = new AtomicLong(8682522807148012L);

Metabolix [24.01.2013 00:34:27]

#

Sami, kiitos korjauksesta, muokkasin vinkkiä. Olinkin lukenut Javan vanhaa dokumentaatiota, jossa kerrottiin, että siemenluku on System.currentTimeMillis(). Oletin, että asia ei olisi muuttunut, ja siksi hieman ihmettelinkin, miksen saa kahta samanlaista Random-oliota.

Javan valmis tapa on siis vielä parempi kuin kumpikaan User137:n ehdotus. Sen pitäisi riittää kaikkeen sellaiseen käyttöön, johon ylipäänsä Random-luokka sopii.

User137 [24.01.2013 00:56:32]

#

Ok, luulin että Random() oletus-alustaja antaa vaan jonkun seed 0 joka kerta.

Pekka Karjalainen [01.02.2013 13:19:47]

#

Vinkistä sattui silmään pari dokumentoimatonta ominaisuutta. Tässä on ohjelma, joka esittelee ne. Käytin annettuja nextInt- ja nextDouble-metodoja siten muutettuna, etteivät ne alla olevassa koodissa ota vastaan Random-tyyppistä parametriä.

Esikatselu ei näytä jostakin syystä mitään.

import java.util.Random;

class RandomTest {

	static Random random = new Random();
	static final int ITERATIONS = 10;

	public static int nextInt(int min, int max) {
		return min + random.nextInt(max - min + 1);
	}

	public static double nextDouble(double min, double max) {
		return min + (max - min) * random.nextDouble();
	}

	public static void main(String[] args) {
		System.out.println("Testaamme nextInt(int, int)-metodia.");
		try {
			for (int i = 0; i < ITERATIONS; i++)
				System.out.println(nextInt(10, 0));
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		}

		System.out.println("Testaamme nextDouble(double, double)-metodia.");
		for (int i = 0; i < ITERATIONS; i++)
			System.out.println(nextDouble(-1e308, 1e308));
	}
}

Jaska [04.02.2013 11:38:06]

#

Väli merkitään yleensä muodossa [3,7] eikä 3–7. Joukko merkitään {3, 4, 5, 6, 7} eikä 3, 4, 5, 6, 7. Joukko {3, 4, 5, 6, 7} ei ole sama asia kuin väli [3,7]. Se on [3,7] \cap \mathbb{Z}.

Metabolix [04.02.2013 13:28:40]

#

Jaska, matemaattinen saivartelusi on tarpeetonta ja melko hölmöä. Vinkki on kirjoitettu suomen kielellä, jossa lukuväliä ilmaistaan viivalla eikä joukkoja erikseen merkitä, ja tyhmäkin tajuaa, että kokonaisluvuista puhuttaessa väli 3–7 käsittää vain kokonaislukuja.

Pekka Karjalainen, kaikenlaisten tarkistusten lisääminen – puhumattakaan ylivuotokikkailuista – on mielestäni tarpeetonta tässä vinkissä, kun aloittelijan tavallisessa käytössä funktiot toimivat jo nyt erinomaisesti. Kiitos kuitenkin hienosta esityksestä.

Jaska [05.02.2013 20:34:12]

#

Saivartelu ja virheellisen tiedon oikaiseminen ovat täysin eri asioita. Jos todella luulet, että pystyt tuosta vaan uudistamaan matematiikan notaatioita, niin onnea vaan yritykseen. Alan itsekin käyttämään niitä sitten kun ne ovat yleisesti hyväksyttyjä. Ongelma olisi varsin helppo korjata poistamalla matemaattinen termin "joukko":

Näistä luvuista saa poimittua yhden ...

Metabolix [05.02.2013 21:06:14]

#

Jaska kirjoitti:

Jos todella luulet, että pystyt tuosta vaan uudistamaan matematiikan notaatioita, niin onnea vaan yritykseen. – – Ongelma olisi varsin helppo korjata poistamalla matemaattinen termin "joukko".

Lue uudestaan: en ole väittänyt uudistavani matematiikan notaatioita, vaan sanoin varsin selvästi, että en käytä lainkaan (enkä yritä käyttää enkä myöskään väitä käyttäväni) matemaattista notaatiota vaan tavallista suomen kieltä. Ainoa ongelma on omassa mielessäsi. Jos luulet, että "joukko" on ainoastaan matemaattinen termi, olet pahasti harhautunut oikeasta elämästä ja tavallisesta kielestä. Sana "joukko" on ollut suomen kielessä varmasti satoja vuosia ennen joukko-opin keksimistä. Jos tässä on jotain epäselvää, suosittelen, että soitat kielitoimiston neuvontapuhelimeen ja kysyt heiltä, mitä sana "joukko" tarkoittaa.

Jaska [05.02.2013 22:40:20]

#

Metabolix kirjoitti:

Jos luulet, että "joukko" on ainoastaan matemaattinen termi, olet pahasti harhautunut oikeasta elämästä ja tavallisesta kielestä.

En luule. Olen käyttänyt itsekin puhekielessä sanaa joukko silloin, kun kuulijalle ei ole väliä, tarkoitanko matemaattista joukkoa vai en. Mulle on aivan sama, mitä ihmiset ajattelevat puhuessani joukosta jalkapallohuligaaneja. Mutta jos kirjoitetussa tekstissä on väliä sillä, kirjoitetaanko x,y,z vai {x,y,z}, en käytä koskaan termiä joukko.

Pekka Karjalainen [08.03.2013 10:31:41]

#

En esittänyt, että funtkioihisi tulisi lisätä tarkistuksia. Jos niihin sellaisia lisättäisiin, en ehdottaisi mitään "kikkailuja" vaan ihan järkeviä testejä. Totesin vain, että niillä on dokumentoimattomia ominaisuuksia, joista vinkissä voisi mainita, ettei niiden löytäminen jäisi vinkin mahdollisen käyttäjän vastuulle. Nythän siellä näyttää maininta olevankin yhdestä asiasta, joten edellinen viestini taisi täyttää tehtävänsä osittain.

Täytyy vain ihmetellä mikä olisi vikana koodivinkkityylissä, jossa metodin määrittelystä johtuvat rajoitukset sen argumenteille ja palautusarvolle esitettäisiin selvästi metodin sisällä argumentteja j atulosta testaavalla logiikalla, joka heittäisi asiaankuuluvat kuvaavat poikkeukset testin epäonnistuessa -- tai vaikkapa käyttäisi Javan assert-lausetta.

Mikä nyt sitten on Javassa se yleisin tapa; en edes tiedä. Muistan jonkun esittäneen, että julkisten metodien tulisi heittää poikkeuksia kun taas assert sopii yksityisiin. Putkavinkkien tapa jättää kaikki testaus pois on toki hyvä yleisenä periaatteena, koska se säästää koodin kirjoittamiseen viemää aikaa ja jättää siten enemmän aikaa pitkäänkin virheidenetsintä- ja -korjausvaiheeseen.

Koodin soveltajan voisi jopa osata olettaa osaavan ottaa pois erillinen tarkistuskoodi ihan itse, jos sen aiheuttaa hirrrveä hidastus ohjelman suoritusnopeuteen huolettaisi niin kovasti. Nopeuttahan tunnetusti kannattaa arvioida silmämääräisesti fiilispohjalta, eikä käyttää mitään apuohjelmia sen mittaamiseen.

Mitä tässä yritin sanoa oli se, että tällaisten pikkutestien tekeminen ihan säännönmukaisesti ei vaikuta minusta vaikealta tai työläältä. En kuitenkaan välitä keskustella asiasta enempää. Jotenkin saan sellaisen kuvan, että edes tällaisen ajatuksen mainitseminen aiheuttaa epämääräistä kiukutusta.

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta