Saltar a contenido

La clase Random y ThreadLocalRandom

Unidad didáctica (UD): UD3 — Programación Orientada a Objetos con la API de Java Resultado de aprendizaje (RA): RA2 — Escribe y prueba programas sencillos, reconociendo y aplicando los fundamentos de la programación orientada a objetos. Criterios de evaluación: CE 2.3 (se han instanciado objetos a partir de clases predefinidas), CE 2.4 (se han utilizado métodos y propiedades de los objetos) y CE 2.8 (se han utilizado constructores) de forma directa, junto con CE 2.6 (se han utilizado parámetros en la llamada a métodos) y CE 2.2 (se han escrito programas simples); refuerzo de CE 2.5 (se han escrito llamadas a métodos estáticos), CE 2.7 (se han incorporado y utilizado librerías de objetos) y CE 2.9 (se ha utilizado el entorno integrado de desarrollo) Sección: 5 de 12 · Evaluación: 1.ª · Unidad completa: 26 h en el IES + 2 h en empresa (FCT) Encaje en el roadmap: quinta sección de UD3, a continuación de la sección 4 (La clase Math) y antes de la sección 6 (Fechas y tiempos: java.time)

En esta sección aprenderás:

  • Por qué Random es el polo opuesto de Math: una clase que no calcula nada, sino que recuerda — y por eso todo su repertorio se pide a un objeto, nunca a la clase.
  • new Random() frente a new Random(semilla): la reproducibilidad, la herramienta profesional que la sección 2 te prometió («reproducir un fallo en una prueba, repetir un sorteo de demostración»).
  • El repertorio de instancia: nextInt(n), nextDouble(), nextBoolean() y nextLong(), con sus rangos exactos y sus trampas.
  • Las sobrecargas de dos argumentos (Java 17): nextInt(1, 7) y la jubilación oficial de la receta de escalado de la sección 4.
  • Random.ints(cantidad, origen, limite): los chorros de azar, el adelanto de la Streams API que la sección 3 prometió cuando te enseñó chars().
  • ThreadLocalRandom.current(): la mejor práctica moderna cuando un programa tiene hilos — y el precio que pagas por usarla.

Necesitas tener fresco (de unidades y secciones anteriores):

  • De la UD1: los tipos primitivos y sus rangos (int, long, double, boolean); los literales con _ separador (600_000).
  • De la UD2: los arrays unidimensionales, el recorrido con for-each y el patrón «contador por casilla»; los bucles y acumuladores; el try-catch con excepciones predefinidas.
  • De la sección 1: los tres pasos de new (reservar en el montón, inicializar con el constructor, devolver la referencia); la diferencia miembro de instancia / miembro estático; y que Random encapsula su semilla y su estado: no hay manera directa de leerlos.
  • De la sección 2: la tabla de constructores de Random (Random() y Random(long seed)); la misión 4 de los apodos con new Random(42); el import java.util.Random; (opcional en los ficheros compactos de JEP 512, pero lo declaramos como debe ser); la regla del receptor; y qué es una fábrica estática.
  • De la sección 3: chars() y las dos operaciones de chorro que no necesitan teoría (count y sum); formatted con %d, %.1f y %n (y la coma decimal del locale español).
  • De la sección 4: Math.random() y su receta de escalado min + (int) (Math.random() * (max - min + 1)); el intervalo [0.0, 1.0) y su corchete abierto; el patrón de extremos con inicialización en el primer dato; y la promesa de la «versión científica» del experimento de los críticos.

5.1 El polo opuesto de Math: la clase que vive de su estado

La sección anterior la cerramos con una frase que hoy conviene poner en el centro de la mesa: «Math.random() es un panel público con una manivela; la manivela y su maquinaria son el tema de la próxima sección». Pues aquí está la maquinaria. java.util.Random es una clase cuyo único tesoro no es lo que sabe calcular, sino lo que recuerda: un motor interno que avanza un poquito con cada número que te entrega. No ofrece fórmulas (sqrt, round...) ni constantes en la pared (PI, E...): ofrece secuencias. Y las secuencias exigen memoria, y la memoria exige objetos. Por eso esta sección es, de toda la unidad, la más instanciadora: aquí no escribirás Clase.metodo(...) ni una sola vez (bueno, casi: hay una honrosa excepción al final), sino generador.metodo(...), una y otra vez.

La comparación con Math es tan limpia que merece tabla propia. Son los dos polos del diseño de clases que has recorrido en cinco secciones:

Math (sección 4) Random (hoy)
Vive en java.lang, se importa solo java.util, con import declarado
Su tesoro Ningún estado: la pared de herramientas Solo estado: el motor interno
Constructores Uno, privado: new Math() no compila Dos públicos: () y (semilla)
Repertorio 99 métodos, todos estáticos 22 métodos propios: 21 de instancia y un estático (from, una rareza conversora que no usaremos)
Receptor Math.metodo(...) generador.metodo(...)
¿final? Sí: ni herencia ni filiales No: admite herencia (su hija SecureRandom desfila al final de la sección)
Nace por... No nace Constructor, como new Random()

Vuelve por un momento a la analogía del gremio de taxistas de la sección 1, porque hoy recupera su papel protagonista. Math era el gremio sin coches: solo el tarifario pegado en la pared. Random es el taxista con taxímetro: cada objeto lleva su cuenta interna, que avanza kilómetro a kilómetro, y las preguntas que le hagas dependen de ese taxímetro, no del tarifario colectivo. Dos objetos Random son dos taxistas distintos, cada uno con su cuenta: ni comparten estado ni se conocen. Y fíjate en lo que la encapsulación (pilar número uno de la sección 1) te está diciendo sin decirlo: del taxímetro solo ves la fachada — los botones nextInt, nextDouble, nextBoolean —, pero la cuenta interna no tiene getter ninguno. Existe, condiciona todo, y no puedes leerla. Puedes fijarla al nacer (la semilla), pero jamás consultarla.

¿Y de dónde había salido todo este discurso? De la sección 2, que ya te hizo leer la página de java.util.Random en la Javadoc para encontrar sus dos constructores. Hoy la abriremos entera. Su anatomía, con lo que ya sabes leer:

  • Declaración: public class Random implements RandomGenerator, Serializable — no es final (puede tener hijas) y desde Java 17 implementa una interfaz, RandomGenerator, de la que hablaremos cuando toque: es tu primer vistazo serio a una pieza que la UD7 abrirá del todo.
  • Constructores: exactamente dos públicos, los de la tabla de la sección 2: Random() («semilla impredecible») y Random(long seed) («la semilla que TÚ le das»).
  • Métodos: veintidós propios — la práctica totalidad de instancia — más lo heredado de RandomGenerator y los nueve eternos de Object.
  • Import: vive en java.util, como Arrays y Scanner, así que nuestros programas empiezan con import java.util.Random; — declaración que, en los ficheros compactos de JEP 512, es casi un ceremonial: java.base viene preimportado y el compilador la encontraría igual. La mantenemos porque es la carta de presentación correcta.

Y el mapa del territorio de hoy, organizado como en las secciones 3 y 4: por verbos, que es como tu cabeza buscará estos métodos cuando programes:

Use mouse to pan and zoom
flowchart TD
    M["El repertorio de Random\nque veras hoy"]
    A["NACER\nelegir constructor:\nsorpresa o reproducibilidad\nnew Random() · new Random(semilla)"]
    B["PEDIR\nnumeros de cada tipo\nnextInt · nextDouble · nextBoolean · nextLong"]
    C["ACOTAR\nrangos con ambos extremos\nnextInt(1, 7) y familia"]
    D["CHORREAR\ncaudales de aleatorios\nints(cantidad, origen, limite)"]
    E["REPARTIR\nun generador por hilo\nThreadLocalRandom.current()"]
    M --> A
    M --> B
    M --> C
    M --> D
    M --> E
    style M fill:#2e7d32,color:#ffffff,stroke:#1b5e20
    style A fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style B fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style C fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style D fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style E fill:#c8e6c9,color:#000000,stroke:#2e7d32

Antes de arrancar, la imagen mental que sostiene toda la sección, porque explica la semilla, la reproducibilidad, la trampa de la llamada extra y hasta por qué existe ThreadLocalRandom: un Random no es una calculadora, es una máquina de secuencias. Le das cuerda al nacer (la semilla); a partir de ahí, cada llamada — la que sea — avanza el motor y te entrega el número que toque:

Use mouse to pan and zoom
flowchart LR
    S["semilla\nla cuerda que le das al nacer"] --> E["motor interno\n48 bits, oculto\n(sin getter)"]
    E -->|"nextDouble()"| F["la maquinaria avanza\nun paso"]
    F --> R["0.7275636800328681\nlo unico que llega a tu pantalla"]
    F --> E2["motor en su nuevo estado"]
    E2 -->|"la siguiente llamada"| F
    style S fill:#e3f2fd,color:#000000,stroke:#1565c0
    style E fill:#fff3e0,color:#000000,stroke:#e65100
    style F fill:#fff3e0,color:#000000,stroke:#e65100
    style R fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style E2 fill:#fff3e0,color:#000000,stroke:#e65100

Si esta imagen te queda clara, el resto de la sección son consecuencias. Empecemos por la más famosa: la cuerda de la máquina ya tiene nombre, y lo conoces desde la sección 2 — la semilla.

5.2 La semilla: elegir el nacimiento, fijar la vida

La sección 2 te hizo elegir constructor sin saber todavía qué estabas eligiendo: «con new Random() compras sorpresa; con new Random(42) compras repetición». Hoy cobramos esa frase con intereses. La Javadoc lo cuenta con una prosa casi de contrato: el constructor sin argumentos «sets the seed of the random number generator to a value very likely to be distinct from any other invocation of this constructor» — fija la semilla a un valor muy probablemente distinto del de cualquier otra llamada al mismo constructor. (¿De dónde sale esa semilla irrepetible? De cosas que ninguna ejecución comparte con la anterior: en la práctica, una mezcla del reloj de alta precisión y de contadores internos de la máquina virtual. El detalle fino no está prometido en la Javadoc — solo el resultado —, así que no lo memorices: quédate con la idea de que nace de «algo que solo pasa una vez».) El constructor con long, en cambio, es cristalino: «using a single long seed» — la semilla eres tú.

Vamos a verlo en carne propia, con el número más cinematográfico de la informática:

import java.util.Random;

void main() {
    Random generador = new Random(42);   // semilla fija: nacimiento decidido por ti

    for (int tirada = 1; tirada <= 10; tirada++) {
        IO.print(generador.nextInt(100) + " ");
    }
    IO.println();   // 30 63 48 84 70 25 5 18 19 93
}

Ejecuta ese programa tres veces. Anota. Ejecuta otras tres. El resultado es siempre la misma decena: 30 63 48 84 70 25 5 18 19 93. En tu máquina y en la del compañero de al lado. Hoy y el trimestre que viene. La semilla no ha «influido» en el azar: lo ha decidido por completo, porque el motor es determinista y su punto de partida eras tú. Cambia la semilla a 7 y saldrá otra decena... también fija. Vuelve a new Random() — sin argumentos — y cada ejecución volverá a sorprenderte: has devuelto la cuerda al reloj.

Y aquí llega la garantía oficial, porque esto no es una casualidad de tu instalación: está escrito. La Javadoc de Random, en su primer párrafo, dice literalmente:

«If two instances of Random are created with the same seed, and the same sequence of method calls is made for each, they will generate and return identical sequences of numbers.»

Dos generadores con la misma semilla y las mismas llamadas producen secuencias idénticas, número a número. Y sigue: «In order to guarantee this property, particular algorithms are specified for the class Random. Java implementations must use all the algorithms shown here for the class Random, for the sake of absolute portability of Java code» — para garantizarla, el algoritmo está especificado, y toda implementación de Java debe usarlo tal cual. Ésa es la diferencia con la cola de precisión de la trigonometría de la sección 4 (donde el último dígito podía variar entre procesadores): tu secuencia de la semilla 42 es portable — la misma en cualquier Java, pasado y futuro, que cumpla la especificación. Cómpralo en una máquina de 2015 con Java 8 y en tu Java 25: misma decena.

Dos experimentos de rigor, porque esta garantía tiene letra pequeña que conviene pisar ahora y no en un examen:

Experimento 1 — dos taxistas con la misma cuenta. La garantía habla de dos instancias. Comprueba que son realmente dos objetos independientes con trayectorias paralelas — no dos ventanas al mismo motor:

import java.util.Random;

void main() {
    Random a = new Random(42);   // taxista A: nace con la cuenta en 42
    Random b = new Random(42);   // taxista B: nace con la cuenta en 42 (otra vez)

    IO.println(a.nextInt(100));   // 30
    IO.println(b.nextInt(100));   // 30  (B recorre SU propia cuenta)
    IO.println(a.nextInt(100));   // 63
    IO.println(b.nextInt(100));   // 63
    IO.println(a.nextBoolean());  // true
    IO.println(b.nextBoolean());  // true
}

Dos objetos en el montón, dos motores, ningún estado compartido — la encapsulación de la sección 1 funcionando a tu favor. Si b avanzara mil llamadas y a solo una, sus siguientes números divergirían: cada cual avanza su motor con sus propias llamadas.

Experimento 2 — la letra pequeña: «same sequence of method calls». La garantía exige las mismas llamadas en el mismo orden. ¿Qué pasa si insertas una llamada extra? Que el motor avanza un paso más y todo lo posterior se desplaza:

import java.util.Random;

void main() {
    Random g = new Random(42);
    for (int i = 1; i <= 5; i++) {
        IO.print(g.nextInt(100) + " ");
    }
    IO.println();   // 30 63 48 84 70

    Random h = new Random(42);
    h.nextInt(100);   // una llamada "extra": un println de mas en la version anterior
    for (int i = 1; i <= 5; i++) {
        IO.print(h.nextInt(100) + " ");
    }
    IO.println();   // 63 48 84 70 25
}

Fíjate bien: no son «otros números», son los mismos números retardados un paso — la secuencia de la semilla 42 desfila igual, pero tu ventana de cinco empieza una posición más tarde. Ésta es la trampa clásica del principiante con semillas: «puse la semilla 42 y ya no me salen los números del acta». Salían — hasta que alguien añadió (o quitó) una llamada antes. La semilla fija la película entera; las llamadas deciden qué plano estás viendo. (Guarda esta idea: reaparece en el quiz.)

setSeed: re-nacer a mitad de vida

¿Y si quieres fijar la semilla de un generador que ya existe? El método setSeed(long) rebobina el motor como si acabara de nacer:

import java.util.Random;

void main() {
    Random g = new Random();      // semilla sorpresa
    g.setSeed(42);                // rebobinado: ahora es como un Random(42) recien nacido

    for (int i = 1; i <= 5; i++) {
        IO.print(g.nextInt(100) + " ");
    }
    IO.println();   // 30 63 48 84 70  (la secuencia de siempre)
}

Tras setSeed(42), la secuencia es exactamente la de un new Random(42) recién creado: el método reemplaza el estado interno por el que genera esa semilla y sigue como si nada. Úsalo con moderación — rebobinar a mitad de programa despista a quien lee — y recuerda el otro lado: no existe getSeed. La semilla se fija o se rebobina, pero jamás se consulta: la encapsulación de Random es de una pieza.

Un apunte de legalidad que sorprende a todo el mundo: la semilla puede ser negativa. new Random(-42) es perfectamente legal — cualquier long sirve — y genera su propia secuencia fija (sus primeros nextInt(100) son 65 46 52 56 36). Las semillas no son «números de la suerte»: son coordenadas en el espacio de secuencias posibles, y hay 2 elevado a 64 de ellas. El 42 manda por tradición (un guiño cultural de The Hitchhiker's Guide to the Galaxy que la informática adoptó hace décadas), no por matemática.

Para qué sirve de verdad: reproducibilidad

Hasta aquí, la mecánica. Ahora, la pregunta profesional: ¿para qué querría nadie anular el azar que acaba de comprar? La sección 2 lo avanzó — «reproducir un fallo en una prueba, repetir un sorteo de demostración» — y hoy lo convertimos en la lista de motivos reales:

  • Depurar lo improbable. Tu programa falla «a veces»: el peor tipo de bug. Fijas la semilla que produjo el fallo y el bug aleatorio se convierte en un fallo determinista: ocurre siempre, en la misma línea, hasta que lo arreglas. Después, quitas la semilla y devuelves el azar.
  • Probar con aserciones exactas. Cuando en la sección 10 llegue JUnit, un test con semilla fija puede afirmar «estos son los diez números que deben salir» — comparación exacta, no «más o menos uniforme». Un test con new Random() no puede prometer nada de eso.
  • Sorteos y demostraciones auditables. Si publicas la semilla junto al programa, cualquiera puede ejecutarlo y comprobar que el resultado oficial es el que la máquina da para esa semilla. Tu práctica guiada de hoy construye exactamente eso: un sorteo con acta.
  • Mundos repetibles. El mismo nivel «aleatorio» de un juego para todo el mundo (Minecraft genera mundos enteros a partir de una semilla que puedes compartir; quien teclea tu semilla visita tu mismo mundo — tu próxima partida puede ser la de otra persona).
Use mouse to pan and zoom
flowchart TD
    S["semilla 42"] --> A["generador A\nnew Random(42)"]
    S --> B["generador B\nnew Random(42)\nen otra maquina, otro dia"]
    A --> A1["30, 63, 48, 84, 70..."]
    B --> B1["30, 63, 48, 84, 70..."]
    A1 --> I["identicos, llamada a llamada:\nla garantia esta en la Javadoc"]
    B1 --> I
    style S fill:#e3f2fd,color:#000000,stroke:#1565c0
    style A fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style B fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style A1 fill:#fff3e0,color:#000000,stroke:#e65100
    style B1 fill:#fff3e0,color:#000000,stroke:#e65100
    style I fill:#2e7d32,color:#ffffff,stroke:#1b5e20
La fórmula que hay dentro de la caja (abrir con curiosidad)

La Javadoc no solo promete el determinismo: publica su receta. Random usa una semilla de 48 bits que avanza con una fórmula congruencial lineal — la receta de los generadores clásicos, citada nada menos que en Donald E. Knuth, The Art of Computer Programming, volumen 2. Cada llamada hace, en esencia:

estado = (estado × 25214903917 + 11) módulo 2 elevado a 48

Ese 25214903917 es la constante 0x5DEECE66D, probablemente la multiplicación más famosa de Java. ¿Quieres ver el motor con el capó abierto? Este programa replica la fórmula a mano — con las cuatro operaciones de la UD1 y un par de operaciones de bits — y produce exactamente los números de un Random(42):

1
2
3
4
5
6
7
8
9
void main() {
    // El estado interno de new Random(42), replicado con la formula de la Javadoc
    long estado = (42L ^ 0x5DEECE66DL) & ((1L << 48) - 1);

    for (int i = 1; i <= 3; i++) {
        estado = (estado * 0x5DEECE66DL + 0xB) & ((1L << 48) - 1);  // avanza el motor
        IO.println((int) (estado >>> 16));   // los 32 bits de arriba: nextInt()
    }
}
1
2
3
-1170105035
234785527
-1360544799

Los tres valores coinciden con new Random(42).nextInt() tres veces seguidas: la «magia» del azar es aritmética de precisión militar, publicada y todo. Ésa es la letra pseudo de «pseudoaleatorio»: números que parecen caóticos y son, en realidad, la sucesión más ordenada del mundo. Nadie te pedirá replicar la fórmula en un examen; pero saber que ahí dentro hay solo multiplicar, sumar y recortar bits cambia tu idea de qué es el azar de un ordenador.

Con la máquina entendida, pasamos al Salón Principal: el repertorio de métodos con los que le pedirás números de cada tipo.

5.3 El repertorio: pedir números de cada tipo

La sección 4 cerró su tabla comparativa prometiendo «el método que va con el tipo de dato que necesites, sin recetas de escalado a mano». Aquí está ese repertorio — el corazón de la clase, todo de instancia, todo con el generador como receptor:

Llamada Devuelve Rango Notas
generador.nextInt() int todo el rango del int Puede salir negativo — y saldrá
generador.nextInt(n) int [0, n) — de 0 a n−1 El índice de array perfecto (sección 2); exige n > 0
generador.nextLong() long todo el rango del long También negativo: ¡diecinueve cifras!
generador.nextDouble() double [0.0, 1.0) La misma ventana que Math.random(), pedida a tu motor
generador.nextFloat() float [0.0, 1.0) El primo de precisión simple
generador.nextBoolean() boolean true / false La moneda al aire
generador.nextGaussian() double campana centrada en 0 Curioso y útil: plegable al final

Un programa para tocarlos todos, con semilla fija para que puedas reproducir la salida en tu máquina (y comprobar que la garantía de la Javadoc funciona):

import java.util.Random;

void main() {
    Random generador = new Random(42);

    IO.println(generador.nextInt());       // -1170105035   (todo el rango: NEGATIVO)
    IO.println(generador.nextInt());       // 234785527
    IO.println(generador.nextLong());      // -5843495416241995736 (negativo y gigante)
    IO.println(generador.nextDouble());    // 0.30871945533265976  (en [0.0, 1.0))
    IO.println(generador.nextBoolean());   // false          (la moneda)
}

Cinco líneas, cinco trampas desactivadas de antemano. Las recorremos.

La trampa del nextInt() pelado. La primera sorpresa del programa es ese -1170105035. ¿Un número aleatorio... negativo? Sí: nextInt() sin argumentos devuelve un int de todo el rango, de -2147483648 a 2147483647, con la mitad de la probabilidad en terreno negativo. Lo mismo hace nextLong() con su rango gigante (nuestro -5843495416241995736 tiene diecinueve cifras y sigue siendo un long legal de sobra). El nextInt() pelado es la herramienta para «un número cualquiera», no para «un número de dado»: cuando quieras rangos, báñalo — la subsección siguiente te da la versión moderna. (Y una curiosidad de relojería: nextInt() sin argumentos es también la llamada más «pura» que existe — consume el motor en su mínimo consumo posible, como viste en el plegable de la fórmula.)

nextInt(n): el rango de la sección 2. La versión con argumento ya la conoces bien: devuelve un entero en [0, n) — de 0 incluido a n excluido. Es la ficha que leíste con Ctrl+Q en la misión 1 de la sección 2, y la razón matemática por la que array[generador.nextInt(array.length)] es un índice garantizado sin ArrayIndexOutOfBoundsException: los índices válidos de un array de longitud n son exactamente 0 a n−1. Para un dado de seis caras, nextInt(6) entrega 0 a 5:

import java.util.Random;

void main() {
    Random dado = new Random(42);

    for (int tirada = 1; tirada <= 10; tirada++) {
        IO.print(dado.nextInt(6) + " ");
    }
    IO.println();   // 2 3 0 2 0 1 5 2 1 5
}

Fíjate en la salida real: aparecen el 0 y el 5, jamás el 6. Si tu problema cuenta desde el 1 (dados, niveles, posiciones humanas), te falta el desplazamiento — y ahí es donde la subsección siguiente moderniza tu vida, pero no te adelantes: entender el [0, n) te lo debes, porque seguirás encontrándolo en código ajeno durante décadas.

nextDouble(): la ventana de siempre. Devuelve un double en [0.0, 1.0) — exactamente la misma ventana y el mismo corchete abierto que Math.random() prometía en la sección 4. La diferencia no está en el número: está en quién te lo da. Math.random() te lo da un generador compartido escondido dentro de Math (el «panel público con una manivela»); nextDouble() te lo da tu motor, con tu semilla y tu secuencia. El patrón de probabilidad de la sección 4 se traduce palabra por palabra: «¿ocurre algo con un 20 % de chances?» es generador.nextDouble() < 0.2.

nextBoolean(): la moneda. La decisión binaria al aire: ¿cara o cruz?, ¿el enemigo ataca o huye?, ¿salta el evento o no? Sin rangos, sin recetas: true o false con un 50 % cada uno. En nuestro programa de arriba salió false — y no es capricho: la moneda también ocupa su casilla exacta en la película de la semilla 42; en la posición siguiente de la secuencia habría salido true.

Y lo que revienta. nextInt(n) exige un límite positivo. La tabla de las tres llamadas suicidas que conviene haber visto una vez (los mensajes son literales de la Javadoc de Java, tal como salen en consola):

Llamada Excepción Mensaje exacto
new Random(42).nextInt(0) IllegalArgumentException bound must be positive
new Random(42).nextInt(-5) IllegalArgumentException bound must be positive
new Random(42).nextDouble(-1.0) IllegalArgumentException bound must be finite and positive

Familia bieneducada de excepciones de rango, primas de la IllegalArgumentException de clamp que ya conociste — y como ellas, capturables con el try-catch de la UD2 si tu programa recibe el límite de fuera.

nextGaussian: cuando el azar tiene forma de campana

Todos los métodos de arriba reparten uniformemente: cada valor de su rango, misma probabilidad. Pero el mundo real muchas veces no es uniforme: la estatura de un grupo, el error de una medición, el tiempo de reacción de un jugador se apiñan alrededor de un centro y se rarecen hacia los extremos — la campana de Gauss. nextGaussian() devuelve números con esa forma: media 0 y desviación típica 1, con valores negativos y positivos repartidos alrededor del cero:

import java.util.Random;

void main() {
    Random generador = new Random(42);

    for (int i = 1; i <= 5; i++) {
        IO.print("%.4f ".formatted(generador.nextGaussian()));
    }
    IO.println();   // 1,1419 0,9194 -0,9499 -1,1070 0,2810
}

Cinco valores alrededor del 0, algunos cerca, alguno lejos: así suena una campana. Si necesitas tu propia media y desviación — estaturas de media 170 y desviación 8, por ejemplo — la traslación es aritmética de la UD1: 170 + 8 * generador.nextGaussian(). El uso estrella en programación de juegos: dar variación natural a atributos de personajes para que ninguno salga idéntico, pero sin extremos absurdos. La plantilla %.4f con formatted — la de la sección 3 — recorta aquí los decimales a cuatro para no inundar la consola; y con el locale español de tu máquina, pinta la coma decimal.

Con el repertorio básico en la mano, queda una asignatura pendiente: los rangos que empiezan donde tú quieres. El dado humano cuenta de 1 a 6, la nota va de 0 a 10, el nivel de 1 a 20... y hasta ahora, tú hacías la aritmética. Java 17 se compadeció de ti.

5.4 Rangos con ambos extremos: la familia de dos argumentos

Recuerda la receta de escalado que la sección 4 te enseñó para Math.random():

Dado un rango de enteros [min, max] (ambos incluidos): min + (int) (Math.random() * (max - min + 1)).

Funciona, la demostraste con un dado y es matemáticamente impecable. Sus defectos son de otra naturaleza: es tuya. La tienes que escribir, la tienes que mantener, la tienes que explicar a quien lee tu código, y cada vez que la escribes es una oportunidad de olvidar un paréntesis o un + 1. Desde Java 17, el API trae la receta incorporada de fábrica: la sobrecarga de dos argumentos.

import java.util.Random;

void main() {
    Random dado = new Random(42);

    for (int tirada = 1; tirada <= 10; tirada++) {
        IO.print(dado.nextInt(1, 7) + " ");
    }
    IO.println();   // 3 4 1 3 1 2 6 3 2 6
}

nextInt(origen, limite) devuelve un entero en [origen, limite): el origen incluido, el límite excluido — el mismo convenio de corchetes que ya manejas. Para el dado: nextInt(1, 7) → de 1 a 6, ambos jugando. Mira la salida real: salen el 1 y el 6, jamás el 7. Diez caracteres menos que la receta, cero paréntesis que mantener, y una lectura directa: «dame un entero entre 1 y 7».

Y ahora, el momento mágico de la subsección. Compara estas dos salidas de la semilla 42, que ya has visto ambas hoy:

nextInt(6)   x10:   2 3 0 2 0 1 5 2 1 5
nextInt(1,7) x10:   3 4 1 3 1 2 6 3 2 6

La segunda es la primera más uno, valor a valor. ¿Casualidad? No: literatura. La implementación de nextInt(origen, limite) es, exactamente, tu receta — un nextInt(limite - origen) y le suma el origen. La comprobación definitiva, con el testigo del double:

1
2
3
4
5
6
7
8
9
import java.util.Random;

void main() {
    Random a = new Random(42);
    IO.println(a.nextDouble());      // 0.7275636800328681

    Random b = new Random(42);
    IO.println(b.nextDouble(0.5));   // 0.36378184001643404  (= 0.7275636800328681 x 0.5)
}

nextDouble(0.5) devuelve el valor en [0.0, 0.5) que sale de multiplicar por el límite el nextDouble() de esa misma posición de la secuencia. La familia de dos argumentos no es magia nueva: es tu receta, ascendida a method — escrita una vez por los ingenieros del JDK, probada por millones de programas y esperándote en el autocompletado. Por eso la receta de la sección 4 no se olvida, se jubila con honores: la entiendes (y la seguirás leyendo en código viejo), pero a partir de hoy la escribes con dos argumentos.

La familia completa — porque nextInt no es la única agraciada — para que la reconozcas en el autocompletado de IntelliJ:

Llamada Rango Ejemplo verificado (semilla 42)
nextInt(origen, limite) [origen, limite) nextInt(1, 7) → 3
nextLong(limite) [0, limite) nextLong(100) → 91
nextLong(origen, limite) [origen, limite) nextLong(1, 101) → 92 (un porcentaje de 1 a 100)
nextDouble(limite) [0.0, limite) nextDouble(0.5) → 0.36378184001643404
nextDouble(origen, limite) [origen, limite) nextDouble(2.0, 5.0) → 4.182691040098604

Fíjate en el detalle fino de los ejemplos: nextLong(1, 101) para «un porcentaje de 1 a 100» — el límite siempre se escribe uno más del último valor que quieres ver, porque está excluido. Ése es el peaje del corchete abierto, y ya lo pagas sin darte cuenta desde la sección 2.

Y lo que revienta, segunda entrega. La familia de dos argumentos aporta sus propias excepciones de rango, con mensajes igual de literales:

Llamada Excepción Mensaje exacto
new Random(42).nextInt(10, 5) IllegalArgumentException bound must be greater than origin
new Random(42).nextInt(7, 7) IllegalArgumentException bound must be greater than origin

Nota la firmeza del diseño: ni siquiera nextInt(7, 7) — un rango «de 7 a 7» que podría devolver siempre 7 — se tolera. Un rango vacío no es una decisión de diseño válida para el API: es un error de quien llama, y el API lo grita.

De dónde salen estos métodos: la interfaz RandomGenerator

Abre la Javadoc de nextInt(int, int) y mira de cerca: no lo declara Random a secas — dice Specified by: ... in java.util.random.RandomGenerator. Desde Java 17 (la gran remodelación del azar de Java, JEP 356), existe una interfaz llamada RandomGenerator que describe «cómo se comporta cualquier generador de números aleatorios», y Random la implementa: es la razón por la que la declaración de la clase luce implements RandomGenerator, Serializable y por la que las sobrecargas de dos argumentos aparecieron de golpe en toda la familia. La gran hermana de la remodelación es una colección de generadores modernos de nombres imposibles (L64X128MixRandom, Splitmix64...) que se piden con RandomGenerator.of("...") — más rápidos y con mejor calidad estadística que el viejo Random, que se conserva por compatibilidad. ¿Y qué es exactamente una interfaz? Esa pregunta merece unidad propia: la UD7 la abre a fondo, porque es la llave del polimorfismo. Hoy basta con saber que existe un «contrato común del azar» y que Random — y ThreadLocalRandom, y SecureRandom — lo firman todos.

SecureRandom: cuando el azar debe ser de verdad

Hay un nivel por encima del que hoy no subiremos, pero que debes saber que existe. SecureRandom (en java.security, hija directa de Random) es el generador criptográficamente fuerte: su semilla se alimenta de fuentes del sistema que un atacante no puede predecir ni reproducir, y sus secuencias resisten análisis. ¿Cuándo hace falta? Siempre que haya dinero o seguridad de por medio: tokens de sesión, contraseñas temporales, identificadores de compra, sorteos con premio legal. Para todo lo demás — juegos, simulaciones, pruebas, las prácticas de este módulo — Random es suficiente y mucho más manejable. La regla de oro, para que la cites si te preguntan: si adivinar tu «aleatorio» le cuesta dinero a alguien, entonces no era aleatorio suficiente. SecureRandom queda citada aquí como StrictMath en la sección 4: conoce su nombre, sabe por qué existe, y sabrás pedirla cuando el dominio del problema la reclame.

Queda el penúltimo verbo del mapa: CHORREAR. Y con él, la promesa más antigua pendiente de esta sección: la que la sección 3 dejó apuntada cuando te enseñó chars().

5.5 Los chorros de azar: ints

Una sola necesidad, dos maneras de pedírsela al generador. Cuando cada número desencadena una decisión — un golpe crítico, un paso de simulación, un evento que salta o no — el patrón natural es el bucle: pides uno, reaccionas, pides otro. Pero hay toda una familia de programas que no quiere decisiones: quiere cantidad. Una simulación con sesenta mil tiradas. Una auditoría con seiscientos mil sorteos. Un experimento con un millón de muestras. Pedirlo de uno en uno funciona; el API te ofrece algo más elegante: pedirlo a chorro.

La sección 3 ya te vendió el concepto con chars(): hay métodos que no devuelven un valor, sino una corriente de valores — un IntStream, un chorro de enteros — que nace de una fuente, recorre una tubería y acaba en un sumidero. Y te prometió que la Streams API se abriría a fondo en la UD5. Random te da hoy el segundo adelanto, y es el más juguetón: la fuente puede ser tu motor de azar.

import java.util.Random;

void main() {
    Random generador = new Random(42);
    int[] tiradas = generador.ints(10, 1, 7).toArray();

    for (int tirada : tiradas) {
        IO.print(tirada + " ");
    }
    IO.println();   // 3 4 1 3 1 2 6 3 2 6
}

Detente en esa salida, porque hoy ya la has visto. Es exactamente la decena del bucle de nextInt(1, 7) de la subsección 5.4: 3 4 1 3 1 2 6 3 2 6. No es casualidad: es el mismo motor, arrancado en la misma casilla (semilla 42), consumiendo las mismas posiciones. El chorro no es una segunda fuente de azar — es tu generador de siempre con una tubería acoplada. La garantía de la Javadoc («misma semilla, mismas llamadas, misma secuencia») se sigue cumpliendo dentro de la tubería.

La firma, leída con calma: ints(long cantidad, int origen, int limite) devuelve un IntStream con cantidad enteros repartidos en [origen, limite) — el convenio de corchetes de toda la unidad, aplicado sin pestañear. Y con el chorro en la mano tienes dos formas de trabajarlo:

  • Recogerlo entero: .toArray() lo vierte en un int[] — un array de la UD2, con todo tu repertorio disponible: for-each, contador por casilla, lo que ya sabes. El programa de arriba es esta forma.
  • Consumirlo al paso: las operaciones de sumidero que la sección 3 estrenó con chars() — .sum() y .count() — funcionan aquí igual, sin escribir un bucle:
import java.util.Random;

void main() {
    int suma = new Random(42).ints(10, 1, 7).sum();
    long cuantos = new Random(42).ints(10, 1, 7).count();

    IO.println("suma: " + suma);              // 31
    IO.println("cuantos: " + cuantos);        // 10
    IO.println("media: " + (suma / 10.0));    // 3.1
}

Un generador fresco por línea, para que cada chorro arranque en la casilla uno de la semilla 42 y los números casen con el programa anterior. Fíjate en el último println: 3.1 con punto — el println crudo siempre pinta el punto decimal, ya lo sabes de la sección 4; si quisieras la coma española, ahí está formatted con %.1f, esperándote como siempre.

La bonoloto en una línea

El ejemplo más vistoso de lo que una tubería bien montada compra con una sola expresión. Sortear una combinación de la bonoloto son seis números distintos, entre 1 y 49, ordenados de menor a mayor. Con las herramientas de la sección 5.3 serían tres preocupaciones y un buen puñado de líneas; con el chorro:

import java.util.Random;
import java.util.Arrays;

void main() {
    int[] combinacion = new Random(2026)
            .ints(1, 50)      // chorro sin fin de numeros en [1, 50)
            .distinct()       // sin repetidos
            .limit(6)         // con que baste seis
            .sorted()         // de menor a mayor, como en el boleto
            .toArray();

    IO.println(Arrays.toString(combinacion));   // [5, 10, 14, 22, 23, 35]
}

Se lee de arriba abajo como una receta: fuente, filtros, cierre. distinct, limit y sorted son operaciones de tubería que aquí solo citamos — su teoría completa tiene casa propia en la UD5, igual que las de chars() —. Lo que sí importa hoy: la semilla 2026 sigue mandando por encima de la tubería. Ejecuta este programa en tu máquina, en la de tu compañero y dentro de un año: la combinación es siempre 5 10 14 22 23 35. El chorro cambia la forma de pedir el azar; no cambia quién manda en él. (Arrays.toString imprime un array de un vistazo — un adelanto de la clase java.util.Arrays, que la sección 9 abrirá a fondo.)

Por qué «chorro»: la metáfora de la tubería

Un stream es agua en movimiento, no un cubo con agua. La imagen que conviene tener en la cabeza tiene tres piezas:

  • La fuente abre el grifo: generador.ints(...), "hola".chars()...
  • La tubería transforma lo que pasa: distinct, limit, sorted... Cada tramo devuelve otra tubería, y por eso se encadenan con puntos.
  • El sumidero cierra el grifo y consume: toArray, sum, count.

Hasta que no abres un sumidero, por el agua no pasa nada: la corriente es perezosa — se calcula justo cuando alguien bebe. Por eso ints(1, 50) sin cantidad es una corriente infinita: un grifo sin cierre. Si en el programa de la bonoloto quitaras el .limit(6), el programa no acabaría nunca: distinct y sorted seguirían esperando a que el chorro termine... y no termina. limit no es adorno ahí: es la llave. (No lo ejecutes sin ella; con que nos creamos el susto basta.) Toda esta mecánica, con sus nombres y sus reglas, es exactamente el capítulo de la UD5: hoy te basta la imagen y las tres operaciones de sumidero.

Con el chorro dominado, el mapa de la sección solo tiene ya un verbo pendiente: REPARTIR. Y repartir, en esta historia, significa literalmente repartir el azar en motores independientes, uno por hilo.

5.6 ThreadLocalRandom: un generador por hilo

Antes de nada, un concepto nuevo en 30 segundos: el hilo (thread). Un programa Java arranca con un único flujo de ejecución — el hilo principal, el que ha recorrido todos tus void main() — y ahí se queda la mayoría de tus programas. Pero los programas ambiciosos — un servidor que atiende a mil usuarios, un juego que pinta y calcula a la vez, una interfaz que no se congela mientras descarga — reparten el trabajo entre varios hilos que avanzan a la vez, como una cocina con varias cocineras trabajando simultáneamente en fogones distintos.

¿Qué tiene eso que ver con Random? Mucho, por una razón que ya conoces de memoria: el generador tiene estado. Ese motor interno que avanza con cada llamada es un recurso compartido si todos los hilos usan el mismo Random. Y un motor único con varios hilos encima se convierte en un cuello de botella: cada avance del estado tiene que hacerse de acuerdo — turno a turno, sin que dos hilos pisen la misma casilla a la vez — y ese ponerse de acuerdo cuesta. Es la fotocopiadora única de la oficina: funciona, pero delante se hace cola. A ese estorbo se le llama contención.

java.util.concurrent.ThreadLocalRandom es la solución de diseño: en vez de un motor compartido, un motor por hilo, guardado en el casillero del propio hilo. La analogía completa: una fotocopiadora por despacho. Nadie hace cola, nadie se estorba, y cada cocinera tiene su fogón.

1
2
3
4
5
6
import java.util.concurrent.ThreadLocalRandom;

void main() {
    int tirada = ThreadLocalRandom.current().nextInt(1, 7);
    IO.println("Has sacado un " + tirada);
}

Mira la línea con los ojos de la sección 2, porque ahí está la pieza que faltaba del rompecabezas de esta unidad: ThreadLocalRandom.current() es una fábrica estática — llamada al colectivo que te devuelve un objeto. La introducción de la sección te lo prometió: «aquí no escribirás Clase.metodo(...) ni una sola vez (bueno, casi: hay una honrosa excepción al final)». Aquí está la excepción, y es honrosa de verdad: current() fabrica (o recupera — dentro de un mismo hilo siempre te devuelve el mismo generador) el motor del hilo que la llama.

El repertorio es el de siempre, porque ThreadLocalRandom es hija directa de Random (hereda de ella, igual que SecureRandom): nextInt(n), nextInt(origen, limite), nextDouble(), nextBoolean(), nextLong(), la familia entera... y también los chorros de ints. Un detalle honesto sobre el programa de arriba: no puedo prometerte su salida. En mi máquina, al escribir esto, salió un 2; en la tuya saldrá lo que quiera el reloj. Y eso no es una debilidad del ejemplo: es exactamente la lección.

El precio. La introducción también lo avisó: «la mejor práctica moderna cuando un programa tiene hilos — y el precio que pagas por usarla». El precio se paga en la moneda de esta sección: la reproducibilidad. ThreadLocalRandom no acepta semillas:

1
2
3
4
5
6
import java.util.concurrent.ThreadLocalRandom;

void main() {
    ThreadLocalRandom generador = ThreadLocalRandom.current();
    generador.setSeed(42);   // UnsupportedOperationException (sin mensaje alguno)
}

setSeed revienta con una UnsupportedOperationException de mensaje vacío — la excepción de «esta puerta está sellada a propósito». No hay new ThreadLocalRandom(42) que valga (su constructor no es público: solo nace por current()), no hay forma humana de fijar su semilla, y por tanto no hay acta, ni garantía de repetición, ni test con resultado exacto. Es un contrato sin letra pequeña: rendimiento a cambio de determinismo. Para lo uno, Random; para lo otro, ThreadLocalRandom.

Tu escenario La herramienta
Necesitas acta: sorteo auditable, demo repetible, test con salida exacta new Random(semilla)
Azar cotidiano en un programa de un hilo (todo lo que hemos escrito hoy) new Random() — o ThreadLocalRandom.current(), ambos correctos
Azar a presión con varios hilos (servidores, juegos) ThreadLocalRandom.current()
Azar con dinero o seguridad de por medio SecureRandom (citada en la subsección 5.4)

Y con esto, el álbum familiar del azar está completo para este curso:

Use mouse to pan and zoom
flowchart TD
    RG["RandomGenerator\nla interfaz: el contrato comun del azar\n(UD7 la abrira a fondo)"]
    R["Random\nel clasico: new Random()\ny new Random(semilla)"]
    TLR["ThreadLocalRandom\nun motor por hilo\ncurrent() · sin semilla"]
    SR["SecureRandom\ncriptograficamente fuerte\n(dinero y seguridad)"]
    RG -->|"lo firman todos"| R
    RG -->|"lo firman todos"| TLR
    RG -->|"lo firman todos"| SR
    R -->|"hereda"| SR
    R -->|"hereda"| TLR
    style RG fill:#2e7d32,color:#ffffff,stroke:#1b5e20
    style R fill:#c8e6c9,color:#000000,stroke:#2e7d32
    style TLR fill:#e3f2fd,color:#000000,stroke:#1565c0
    style SR fill:#fff3e0,color:#000000,stroke:#e65100
Cómo se estorban los hilos en un motor único (y cómo lo resuelve el JDK)

El motor de Random avanza así: lee su estado, calcula el siguiente, lo escribe. Si dos hilos lo hacen a la vez sobre el mismo estado, uno pisa al otro y la secuencia se corrompería. El JDK moderno lo evita con técnicas sin bloqueo: cada hilo intenta el avance completo y, si descubre que otro le ganó la casilla, reintenta con el estado nuevo. Correcto, sí — las secuencias salen bien —, pero bajo presión cada reintento es trabajo tirado: energía que se gasta en ponerse de acuerdo en vez de en dar números. ThreadLocalRandom elimina el problema por la raíz: si cada hilo tiene su motor, no hay nada que acordar. La solución de arquitectura (repartir el recurso) vence a la solución de coordinación (ponerse de acuerdo): una lección de diseño que reutilizarás toda la vida profesional, con o sin azar de por medio.

¿Y cuándo veré hilos de verdad?

En este módulo, casi ni los rozamos: nuestros programas son de un hilo y así seguirán hasta el final del curso. Los hilos tienen su propio espacio más adelante en tu formación — cuando construyas interfaces que no se congelen, servicios que atiendan a muchos clientes a la vez, tareas en segundo plano —. Lo que hoy te llevas es la pieza que entonces importará: cuando ese día llegue, el azar ya tiene dueño: ThreadLocalRandom.current(), y la razón de su diseño. (JavaFX, el contexto gráfico de este mismo ciclo, te hará tocar esto antes de lo que crees.)

El reparto está hecho, el mapa completo. Solo queda hacer con todo ello algo que merezca la pena: una práctica con acta notarial.

5.7 Práctica guiada: el sorteo con acta

El escenario. El taller de 2.º DAM sortea tres premios de material entre el alumnado de un grupo de doce. La dirección, escarmentada de sorteos caseros, pide una cosa: que el sorteo sea auditable — que cualquiera pueda reproducirlo y comprobar que el resultado oficial es el que la máquina da. Acabas de aprender exactamente cómo se construye eso: un programa, una semilla pública y un resultado publicado. Ese trío es el acta. (La subsección 5.2 te lo adelantó: «tu práctica guiada de hoy construye exactamente eso».)

El bombo será siempre este array — doce pioneras y pioneros de la informática, para que el sorteo también repase nombres que te cruzarás toda la vida:

1
2
3
4
String[] alumnado = {
    "Ada", "Alan", "Grace", "Linus", "Margaret", "Dennis",
    "Barbara", "Ken", "Radia", "Guido", "Annie", "Tim"
};

(Ada Lovelace, Alan Turing, Grace Hopper, Linus Torvalds, Margaret Hamilton, Dennis Ritchie, Barbara Liskov, Ken Thompson, Radia Perlman, Guido van Rossum, Annie Easley y Tim Berners-Lee.) Y la semilla pública del acta, para que todo el mundo juegue con el mismo bombo: 2026. Cada misión es plegable, como siempre: inténtalo tú antes de desplegar la solución.

Misión 1 — El acta del bombo

El encargo: sortear un ganador con la semilla 2026, repetir el sorteo con un bombo nuevo y comprobar que sale lo mismo, y probar qué pasaría con la semilla 2027.

import java.util.Random;

void main() {
    String[] alumnado = {
        "Ada", "Alan", "Grace", "Linus", "Margaret", "Dennis",
        "Barbara", "Ken", "Radia", "Guido", "Annie", "Tim"
    };

    Random bombo = new Random(2026);
    String ganador = alumnado[bombo.nextInt(alumnado.length)];
    IO.println("ACTA DEL SORTEO - semilla 2026");
    IO.println("Ganador del material: " + ganador);       // Tim

    Random segundoBombo = new Random(2026);
    String comprobacion = alumnado[segundoBombo.nextInt(alumnado.length)];
    IO.println("Comprobacion (bombo nuevo): " + comprobacion);   // Tim otra vez

    Random bomboVecino = new Random(2027);
    IO.println("Con la semilla 2027 ganaria: "
            + alumnado[bomboVecino.nextInt(alumnado.length)]);   // Guido
}
1
2
3
4
ACTA DEL SORTEO - semilla 2026
Ganador del material: Tim
Comprobacion (bombo nuevo): Tim otra vez
Con la semilla 2027 ganaria: Guido

Tres observaciones de auditor. Primera: nextInt(alumnado.length) es la garantía matemática de la sección 2 — array[generador.nextInt(array.length)] no puede salirse del array. Segunda: el bombo nuevo con la misma semilla repite el resultado: la garantía de la Javadoc funcionando como acta notarial. Tercera: cambia la semilla y cambia el ganador — la semilla es una coordenada, no un «toque de suerte». Publicar 2026 junto al resultado es todo lo que la auditoría necesita.

Misión 2 — El pack del ganador

El encargo: el ganador también se lleva un pack de material sorteado con un dado (seis packs posibles: del 1 al 6). El equipo organizador discute: ¿se tira el dado con el mismo bombo, después del ganador, o se monta un bombo nuevo para el dado?

import java.util.Random;

void main() {
    String[] alumnado = {
        "Ada", "Alan", "Grace", "Linus", "Margaret", "Dennis",
        "Barbara", "Ken", "Radia", "Guido", "Annie", "Tim"
    };

    Random bombo = new Random(2026);
    String ganador = alumnado[bombo.nextInt(alumnado.length)];
    int pack = bombo.nextInt(1, 7);        // el dado, CON EL MISMO bombo
    IO.println(ganador + " se lleva el pack " + pack);     // Tim se lleva el pack 5

    Random bomboFresco = new Random(2026);   // el dado, con bombo NUEVO
    IO.println("Con bombo nuevo, el dado daria: "
            + bomboFresco.nextInt(1, 7));                   // 6
}
Tim se lleva el pack 5
Con bombo nuevo, el dado daria: 6

Aquí está, en vivo y en directo, el experimento 2 de la subsección 5.2: misma semilla, distinto guion de llamadas, distinto resultado. El pack 5 no es «la segunda cifra de la semilla 2026»: es la segunda cifra de ese programa concreto, que hizo una llamada de ganador antes. El programa con bombo fresco — guion distinto — da 6. Ninguno miente; simplemente no son el mismo acta.

La lección profesional: cuando publicas un acta, publica también el guion de llamadas — es decir, el programa entero, exactamente como se ejecutó. «Semilla 2026» sin el programa no significa nada: la semilla fija la película, las llamadas deciden qué plano estás viendo.

Misión 3 — Los tres premios: barajar el bombo

El encargo: tres premios distintos (1.º, 2.º y 3.º) para tres personas distintas. La forma honesta de hacerlo: barajar el array completo y dar los tres primeros puestos — el algoritmo clásico Fisher-Yates, que recorre el array desde el final y va dejando cada casilla asentada con un intercambio al azar entre las no decididas.

import java.util.Random;

void main() {
    String[] alumnado = {
        "Ada", "Alan", "Grace", "Linus", "Margaret", "Dennis",
        "Barbara", "Ken", "Radia", "Guido", "Annie", "Tim"
    };

    Random bombo = new Random(2026);
    String[] copia = alumnado.clone();   // baraja la COPIA: el original queda intacto

    for (int i = copia.length - 1; i > 0; i--) {   // desde el final hacia el principio
        int j = bombo.nextInt(i + 1);             // una casilla SIN decidir: [0, i]
        String temporal = copia[i];               // intercambio clasico de tres tiempos
        copia[i] = copia[j];
        copia[j] = temporal;
    }

    IO.println("Orden barajado del acta 2026:");
    for (String nombre : copia) {
        IO.print(nombre + " ");
    }
    IO.println();
    IO.println("1er premio: " + copia[0]);   // Margaret
    IO.println("2o premio:  " + copia[1]);   // Guido
    IO.println("3er premio: " + copia[2]);   // Annie
}
1
2
3
4
5
Orden barajado del acta 2026:
Margaret Guido Annie Linus Alan Dennis Grace Barbara Ada Ken Radia Tim
1er premio: Margaret
2o premio:  Guido
3er premio:  Annie

Fíjate en los dos detalles del bucle. clone() fabrica una copia del array — barajar la copia conserva el original por si otra misión lo necesita (¿te suena? es la misma prudencia de no estropear el dato de entrada). Y nextInt(i + 1): la ventana se ciñe a las casillas aún sin decidir, que son exactamente de 0 a i. Al empezar, la casilla 11 elige entre las doce; cuando llega el turno de la casilla 1, solo quedan ella y la 0. Cada orden posible del array tiene exactamente la misma probabilidad — no es magia: es multiplicar ventanas decrecientes, 12 × 11 × 10 × ... × 1.

El plegable del escéptico: por qué el bucle va hacia atrás

Todo el mundo «sabe» barajar: recorro el array y cambio cada casilla con otra cualquiera elegida al azar. ¿Qué tiene de malo? Que es un sorteo sesgado: no todos los órdenes salen con la misma probabilidad. Con doce nombres el sesgo es sutil, así que el experimento lo monta con tres (seis órdenes posibles; si el barajado fuera justo, cada orden saldría una de cada seis veces). Seiscientas mil barajadas con cada método:

1
2
3
4
Fisher-Yates (correcto):  orden original 100317 / 600000  ->  0.167  (teoria: 0.167)
Ingenuo (cada i con      ABC     88643   ACB     111118   BAC     111232
cualquiera):             BCA     110791   CAB     89087    CBA     89129
                          (cada orden deberia salir unas 100000 veces)

El barajado ingenuo fabrica órdenes favoritos: ACB, BAC y BCA salen un 11 % más de lo justo, y ABC, CAB y CBA un 11 % menos. Fisher-Yates, en cambio, clava la teoría hasta la tercera decimal. La moraleja trasciende al azar: cuando algo debe ser justo, el algoritmo se diseña, no se improvisa — y el «por qué» de cada línea del diseño (ese i + 1, ese bucle hacia atrás) es exactamente lo que lo hace justo.

Misión 4 — La auditoría del bombo

El encargo: la dirección pregunta lo que cualquier escéptico preguntaría: ¿este bombo es justo? Auditoría: seiscientos mil sorteos y un contador por persona.

import java.util.Random;

void main() {
    String[] alumnado = {
        "Ada", "Alan", "Grace", "Linus", "Margaret", "Dennis",
        "Barbara", "Ken", "Radia", "Guido", "Annie", "Tim"
    };

    Random auditor = new Random(2026);            // tambien auditable, claro
    int[] sorteos = auditor.ints(600_000, 0, 12).toArray();

    int[] contador = new int[12];                 // el contador por casilla de la UD2
    for (int persona : sorteos) {
        contador[persona]++;
    }

    for (int i = 0; i < contador.length; i++) {
        IO.println(alumnado[i] + ": " + contador[i]);
    }
    IO.println("Media teorica: " + (600_000 / 12.0));   // 50000.0
}
1
2
3
4
5
Ada: 49857        Margaret: 50459     Ken: 50063
Alan: 50173       Dennis: 49899       Radia: 49662
Grace: 49957      Barbara: 50113      Guido: 49857
Linus: 49755      Annie: 49908        Tim: 50297
Media teorica: 50000.0

Lectura de auditor: la persona más agraciada, Margaret, ganó 50 459 veces; la más postergada, Radia, 49 662. La diferencia entre extremos es de 797 sorteos — un 1,6 % sobre la media teórica de 50 000. Eso no es favoritismo: es exactamente el vaivén estadístico que cabe esperar de 600 000 tiradas honestas. El chorro ints(600_000, 0, 12) produjo además una suma total de 3 300 158 — media 5,500263... por sorteo, clavada al centro de [0, 12) que es 5,5 (los dos chorros de sumidero también juegan: new Random(2026).ints(600_000, 0, 12).sum() y .count() te lo confirman en dos líneas). Resultado del informe: bombo justo.

Misión 5 — Los críticos, la versión científica (la promesa de la sección 4)

El encargo: cierra la deuda más antigua: en la sección 4 simulaste cinco ataques «con un 20 % de crítico» y yo te prometí «la versión científica del experimento». Aquí está: mil tandas de cinco ataques, contando cuántas tandas obtienen 0, 1, 2... críticos. La teoría dice que, con cinco ataques al 20 %, la media debe rondar un crítico por tanda (5 × 0,2).

import java.util.Random;

void main() {
    Random generador = new Random(42);       // acta: cualquiera puede repetir el experimento
    int[] tandas = new int[6];               // criticos por tanda: 0..5

    for (int tanda = 1; tanda <= 1000; tanda++) {
        int criticos = 0;
        for (int ataque = 1; ataque <= 5; ataque++) {
            if (generador.nextDouble() < 0.2) {
                criticos++;
            }
        }
        tandas[criticos]++;                  // contador por casilla, una vez mas
    }

    for (int k = 0; k <= 5; k++) {
        IO.println("tandas con " + k + " criticos: " + tandas[k]);
    }
    IO.println("Media: %.2f criticos por tanda"
            .formatted((tandas[1] + 2 * tandas[2] + 3 * tandas[3]
                    + 4 * tandas[4] + 5 * tandas[5]) / 1000.0));
}
1
2
3
4
5
6
7
tandas con 0 criticos: 363
tandas con 1 criticos: 383
tandas con 2 criticos: 197
tandas con 3 criticos: 55
tandas con 4 criticos: 2
tandas con 5 criticos: 0
Media: 0,95 criticos por tanda

Mira la forma de la tabla: tandas sin críticos (363) y con uno (383) dominan; dos (197) ya es menos frecuente; tres (55), raro; cuatro, rarísimo (2); y cinco seguidos, ninguna en mil tandas. Eso es una campana asomando — el primo discreto de la campana del plegable nextGaussian. La media sale 0,95 frente al 1,0 teórico: en mil tandas, el azar honesto se acerca pero no clava — compáralo con la misión 4, donde 600 000 tiradas afinaron muchísimo más. El tamaño de la muestra manda: la estadística converge despacio, y quien te enseñe un experimento de veinte tiradas «demostrando» una probabilidad te está contando un cuento.

Y el colofón, con la firma de esta sección: reescribe el sorteo de la misión 1 con ThreadLocalRandom.current() y ejecútalo dos veces. En mi máquina salieron Barbara y luego Linus; en la tuya saldrán otros. Ningún acta, ninguna garantía, ningún test posible: el generador rápido de los hilos, incapaz de notariar nada. Para firmar, Random con semilla; para correr, ThreadLocalRandom. Fin del sorteo.

Si has llegado hasta aquí con las cinco misiones, fíjate en lo que tienes en la mano: un sorteo publicado que cualquiera puede ejecutar y comprobar, una auditoría estadística del bombo, y un experimento científico con acta. Eso, en el mundo profesional, tiene nombre: reproducibilidad — y es la diferencia entre «confía en mí» y «compruébalo tú».

5.8 Resumen de la sección

  1. Random es el polo opuesto de Math: no ofrece fórmulas sino secuencias, y las secuencias exigen memoria — estado interno —, y el estado exige objetos. De ahí que sus 22 métodos propios sean de instancia (21, más la rareza estática from) y que el receptor sea siempre generador.metodo(...).
  2. La semilla fija la película entera: dos generadores con la misma semilla y las mismas llamadas producen secuencias idénticas, en cualquier Java, por especificación. Esa garantía es la base de la reproducibilidad: depurar lo improbable, tests exactos, sorteos auditables, mundos compartidos.
  3. La letra pequeña de la garantía: «la misma secuencia de llamadas». Una llamada extra desplaza todo lo posterior: los mismos números retardados una posición, no «otros números».
  4. nextInt() pelado recorre todo el rango del int (negativos incluidos); nextInt(n) entrega [0, n); y desde Java 17, la familia de dos argumentos — nextInt(origen, limite) y compañía — entrega [origen, limite) de fábrica: tu receta de escalado, jubilada con honores.
  5. nextDouble() reparte en [0.0, 1.0) (la ventana de Math.random(), servida por tu motor); nextBoolean() es la moneda; nextGaussian() la campana. Las llamadas imposibles revientan con IllegalArgumentException de mensajes literales (bound must be positive, bound must be greater than origin).
  6. setSeed rebobina el motor a mitad de vida, pero no existe getSeed: la semilla se fija, jamás se consulta. La encapsulación de Random es de una pieza.
  7. generador.ints(cantidad, origen, limite) abre un chorro (IntStream): mismo motor, misma semilla, misma secuencia que el bucle equivalente. Se recoge con toArray() o se consume con sum() y count(). La teoría completa de la tubería (distinct, limit, sorted, corrientes infinitas) vive en la UD5.
  8. ThreadLocalRandom.current() es una fábrica estática que entrega a cada hilo su propio generador: la solución de arquitectura a la contención del motor único. Su precio, sellado con una UnsupportedOperationException: no acepta semillas, no firma actas.
  9. El criterio de elección: acta → Random con semilla; azar cotidiano de un hilo → cualquiera de los dos; presión multihilo → ThreadLocalRandom; dinero o seguridad → SecureRandom.
  10. Un sorteo auditable publica el trío completo: programa, semilla y resultado. Sin el programa, la semilla no significa nada: el guion de llamadas también forma parte del acta.

Glosario de la sección

Término Definición en una línea
Semilla Coordenada inicial que decide la secuencia completa de un generador
Reproducibilidad Misma semilla + mismas llamadas = misma salida, en cualquier máquina y Java
Determinismo El motor no sortea: calcula — la letra pseudo de «pseudoaleatorio»
Motor interno El estado oculto (48 bits) que avanza un paso con cada llamada
LCG (generador congruencial lineal) La receta publicada en la Javadoc: multiplicar, sumar, recortar módulo 2 elevado a 48
Secuencia de llamadas El otro ingrediente de la garantía: qué se pide, y en qué orden
setSeed(long) Rebobinar el motor de un generador vivo, como si renaciera con esa semilla
Corchete abierto [origen, limite) Origen incluido, límite excluido: el convenio de nextInt(n), ints(...), Math.random()
Familia de dos argumentos nextInt(origen, limite) y compañía: rangos con ambos extremos útiles, desde Java 17
nextGaussian() Azar en campana: media 0, desviación 1, valores apiñados en el centro
Chorro / IntStream Corriente de valores que fluye de una fuente a un sumidero
Operación terminal La que consume el chorro y cierra el grifo: toArray, sum, count
Operación intermedia La que transforma la tubería y devuelve otra tubería: distinct, limit, sorted (UD5)
Corriente infinita ints() sin tamaño: grifo sin cierre — necesita un limit que lo cierre
Hilo (thread) Flujo de ejecución independiente; tu void main() es el hilo principal
Contención El estorbo de varios hilos avanzando un único motor compartido
ThreadLocalRandom.current() Fábrica estática que da a cada hilo su generador particular
RandomGenerator La interfaz-contrato común del azar, firmada por toda la familia (UD7)
Acta auditada Programa + semilla + resultado publicados: la prueba de un sorteo
Fisher-Yates El barajado justo: cada casilla decide entre las aún no decididas

Checklist: al terminar, debes ser capaz de...

  • [ ] Explicar por qué Random necesita objetos y Math no, con la palabra estado en el centro del argumento (CE 2.1, CE 2.3).
  • [ ] Predecir la salida de dos new Random(42) sometidos a las mismas llamadas (CE 2.2).
  • [ ] Diagnosticar el síntoma «ya no me salen los números del acta» buscando la llamada extra desplazada (CE 2.2).
  • [ ] Predecir los valores posibles de nextInt(6) y de nextInt(1, 7) sin mirar apuntes (CE 2.2, CE 2.6).
  • [ ] Escribir un rango [min, max] de enteros con la sobrecarga correcta y el límite bien puesto (max + 1) (CE 2.6).
  • [ ] Nombrar qué lanzan nextInt(0) y nextInt(7, 7), con sus mensajes literales (CE 2.2).
  • [ ] Traducir «ocurre algo con un 20 % de probabilidad» a generador.nextDouble() < 0.2 (CE 2.2, CE 2.6).
  • [ ] Recoger un ints(...) en array con toArray y consumirlo con sum y count (CE 2.2, CE 2.7).
  • [ ] Construir un sorteo auditable: semilla pública, programa publicado, resultado cotejable (CE 2.2, CE 2.3).
  • [ ] Barajar un array con Fisher-Yates y explicar por qué el bucle va hacia atrás y la ventana se ciñe (CE 2.2).
  • [ ] Elegir entre Random, ThreadLocalRandom y SecureRandom ante un escenario dado (CE 2.5, CE 2.7).
  • [ ] Explicar por qué ThreadLocalRandom sella setSeed con UnsupportedOperationException (CE 2.2).
  • [ ] Leer la ficha Javadoc de un método de Random que no hayamos visto y deducir rango, devolución y excepciones (CE 2.7, CE 2.9).

Lo que hoy quedó citado, y dónde vive

Tema mencionado hoy Dónde se desarrolla
Streams a fondo: distinct, limit, sorted, corrientes infinitas, Optional UD5
La interfaz RandomGenerator y el polimorfismo UD7
SecureRandom Citada (5.4 y hoy); sin desarrollo en el itinerario del módulo
Generadores modernos (L64X128MixRandom, Splitmix64...) Citados; fuera del itinerario
Tests con semilla fija y JUnit Sección 10 de esta unidad
Hilos y concurrencia Más adelante en el ciclo (interfaces gráficas, servicios)
Ejercicios globales con Random Sección 12 de esta unidad

5.9 Autoevaluación de la sección

¿Esto no era la sección 11?

Una vez más: el test oficial de repaso de la unidad vive en la sección 11, dividido por secciones para el repaso de conjunto. Este bloque es una autoevaluación rápida para hacer justo después de leer, sin material y sin presión: si fallas algo, vuelve a la subsección correspondiente antes de continuar. El orden de las preguntas sigue el orden de la sección.

#

Pregunta 1

¿Por qué el repertorio de Random se pide a un objeto y no a la clase?

#

Pregunta 2

Ejecutas new Random(42) en tu máquina y tu compañero hace lo mismo en la suya, con las mismas llamadas. ¿Qué ocurre?

#

Pregunta 3

Misma semilla 42, pero alguien inserta una llamada extra antes de tu bucle de cinco números. ¿Qué ves ahora?

#

Pregunta 4

¿Qué valores puede devolver generador.nextInt(6)?

#

Pregunta 5

Para simular un dado humano de 1 a 6 con la familia de dos argumentos, escribes...

#

Pregunta 6

¿Qué tienen en común generador.nextDouble() y Math.random()?

#

Pregunta 7

¿Qué devuelve generador.ints(10, 1, 7)?

#

Pregunta 8

¿Qué ocurre al ejecutar ThreadLocalRandom.current().setSeed(42)?

#

Pregunta 9

Tu prueba unitaria necesita afirmar "estos son exactamente los diez números que deben salir". ¿Qué generador usas?

#

Pregunta 10

¿Se puede consultar la semilla de un Random ya creado?

Cuando las diez preguntas te salgan sin dudar, la sección del azar está cerrada: dominas el generador que se instancia, la semilla que convierte el azar en acta, los rangos de corchete abierto, los chorros que asoman a la UD5 y el reparto por hilos de la casa moderna. Estás listo para la sección 6 — Fechas y tiempos: java.time, donde la unidad cambia de tercio por completo: del azar pasamos al tiempo. Conocerás la historia de jubilación más famosa del JDK (la de Date y Calendar, prometida en la sección 2), el paquete donde todo es inmutable como String y todo nace por fábricas como LocalDate.now() — el patrón contrario al Random que hoy has instanciado hasta la saciedad —, y aprenderás a calcular con calendarios, horas, intervalos y zonas horarias sin volver a escribir un if de febrero en tu vida.