UD2 · Sección 5 — La clase Random y ThreadLocalRandom
| Dato | Valor |
|---|---|
| Unidad Didáctica | UD2 — Programación Orientada a Objetos con la API de Java |
| Resultado de Aprendizaje | RA2 — Escribe y prueba programas sencillos, reconociendo y aplicando los fundamentos de la POO orientada a objetos. |
| Sección | 5 de 10 — La clase Random y ThreadLocalRandom |
| Evaluación | 1.ª Evaluación |
| Duración estimada | 2 horas lectivas |
| Criterios de evaluación cubiertos | CE 2.2 (escribe programas simples), CE 2.3 (instancia objetos a partir de clases predefinidas), CE 2.4 (utiliza métodos y propiedades), CE 2.5 (llamadas a métodos estáticos), CE 2.6 (utiliza parámetros), CE 2.8 (utiliza constructores) |
| Prerrequisitos | UD1 completa, UD2 Secciones 1, 2, 3 y 4 |
| Paquete del código de ejemplo | dam.programacion.ud2 |
| Herramientas | JDK 25, IntelliJ IDEA Community, Maven 3.9.x |
1. Punto de partida
En la sección anterior conociste Math.random(), una forma rápida de generar números aleatorios en el rango [0.0, 1.0). Es cómoda pero limitada: no puedes generar directamente enteros, booleanos o long; no puedes reproducir secuencias; y usa internamente un único Random compartido por toda la aplicación, lo que puede ser un cuello de botella si muchos hilos lo usan a la vez.
En esta sección vas a conocer dos clases que resuelven esas limitaciones: Random (la clase clásica, plenamente funcional) y ThreadLocalRandom (la versión moderna y recomendada para la mayoría de casos). Ambas viven en java.util y comparten la misma API, así que aprender una es aprender casi la otra.
Al terminar la sección serás capaz de: crear un generador de aleatorios con y sin semilla, generar valores de distintos tipos (enteros, decimales, booleanos, long), entender por qué la reproducibilidad es importante en tests y depuración, y elegir correctamente entre Random y ThreadLocalRandom.
2. ¿Qué es un número "aleatorio" para una computadora?
Antes de usar la clase, conviene aclarar una idea importante: una computadora no genera números verdaderamente aleatorios (salvo que use hardware especializado). Lo que genera son números pseudoaleatorios: secuencias que parecen aleatorias (pasan tests estadísticos) pero en realidad están calculadas a partir de una fórmula matemática determinista.
La fórmula parte de un valor inicial llamado semilla (seed). A partir de la semilla, el generador produce una secuencia que, mientras la fórmula sea buena, parece impredecible. Si conoces la semilla y la fórmula, puedes reproducir exactamente la misma secuencia. Esto tiene una consecuencia práctica importantísima que veremos enseguida: la reproducibilidad.
Analogía de la receta. Imagina que "aleatorio" fuera una receta de cocina que produce un plato distinto cada vez, pero la receta se prepara a partir de un ingrediente inicial (la semilla). Si dos cocineros parten del mismo ingrediente inicial, ambos obtienen exactamente la misma secuencia de platos, en el mismo orden. Si parten de ingredientes distintos, los platos son diferentes. La "apariencia aleatoria" viene de que la receta mezcla los sabores de forma compleja.
flowchart LR
S1["Semilla 42"] --> R["Formula matematica<br/>(PRNG)"]
S2["Semilla 99"] --> R2["Formula matematica<br/>(PRNG)"]
R --> O1["Secuencia 1<br/>7, 19, 3, 88, ..."]
R2 --> O2["Secuencia 2<br/>51, 2, 14, 73, ..."]
S3["Misma semilla 42"] --> R3["Misma formula"]
R3 --> O3["Misma secuencia 1<br/>7, 19, 3, 88, ..."]
3. La clase Random
3.1. Import y primer ejemplo
Random vive en java.util, así que necesita import:
Creación de un generador y generación de valores básicos:
Cada vez que ejecutes el programa, los valores serán distintos (salvo que uses semilla, lo veremos enseguida).
3.2. Constructores: con y sin semilla
Random tiene dos constructores, lo que encaja perfectamente con el CE 2.8 (utiliza constructores):
| Constructor | Qué hace | Cuándo usarlo |
|---|---|---|
Random() |
Crea un generador con semilla "aleatoria" (basada en el reloj del sistema) | Caso general: programas de producción, juegos, simulaciones |
Random(long semilla) |
Crea un generador con una semilla fija | Tests, depuración, simulaciones reproducibles |
Sin semilla: secuencia distinta cada ejecución
Internamente, new Random() usa como semilla una combinación del tiempo actual en nanosegundos y unos valores internos de la JVM. Por eso dos ejecuciones dan resultados distintos.
Con semilla: secuencia reproducible
Dos objetos Random creados con la misma semilla producen exactamente la misma secuencia. Esto es valiosísimo para:
- Tests automáticos: si un test genera datos aleatorios y falla, poder reproducir la misma secuencia te permite depurar.
- Depuración: un bug que aparece "a veces" se puede reproducir siempre si guardas la semilla.
- Simulaciones científicas: dos investigadores deben poder repetir el mismo experimento.
- Juegos competitivos: en juegos por turnos, compartir la semilla permite a ambos jugadores ver el mismo "tablero aleatorio".
Analogía de la lista de reproducción. Imagina que un Random es una lista de reproducción infinita de números. La semilla es el "código de la lista": si dos personas introducen el mismo código, obtienen exactamente la misma lista en el mismo orden. Si introducen códigos distintos, las listas son diferentes.
3.3. Tabla de métodos principales
| Método | Qué devuelve | Rango |
|---|---|---|
nextInt() |
int |
Todo el rango de int (positivos y negativos) |
nextInt(int bound) |
int |
[0, bound) — bound es exclusivo |
nextLong() |
long |
Todo el rango de long |
nextDouble() |
double |
[0.0, 1.0) |
nextFloat() |
float |
[0.0, 1.0) |
nextBoolean() |
boolean |
true o false (50/50) |
nextGaussian() |
double |
Distribución normal (media 0, desviación 1) |
ints(int cantidad, int min, int max) |
IntStream |
Stream de enteros (adelanto de UD6) |
setSeed(long semilla) |
void |
Cambia la semilla del generador |
Ejemplos:
Atención: nextInt(int bound) requiere que bound sea positivo. Si le pasas 0 o un negativo, lanza IllegalArgumentException.
3.4. Generar enteros en un rango [min, max] (inclusivo)
El patrón más común. Hay dos formas, dependiendo de la versión de Java:
Forma clásica (todas las versiones)
La fórmula general es:
Donde max - min + 1 es el número de valores posibles en el rango [min, max] (ambos inclusive).
Forma moderna (Java 17+)
Desde Java 17, nextInt admite dos argumentos:
Es más legible pero requiere recordar que el segundo argumento es exclusivo. Por eso nextInt(1, 7) da 1..6, no 1..7.
3.5. Distribución uniforme vs. gaussiana
nextInt,nextDouble: distribución uniforme. Cada valor dentro del rango tiene la misma probabilidad de salir.nextGaussian: distribución normal (campana de Gauss). Los valores cercanos a 0 son mucho más probables que los valores grandes. Útil para modelar alturas, tiempos, errores de medición.
Analogía de los dados vs. la altura. Tirar un dado de 6 caras es distribución uniforme: cada cara sale con probabilidad 1/6. En cambio, la altura de las personas es gaussiana: la mayoría mide entre 1,60 y 1,80, pocas personas miden 1,50 o 1,90, y casi nadie mide 1,40 o 2,00.
3.6. Random.ints(...): adelanto de Streams
El método ints devuelve un IntStream, que es un flujo de enteros. Es un adelanto de la Streams API que se verá a fondo en la UD6. Aquí lo mencionamos para que lo reconozcas si lo ves:
No te preocupes por entenderlo ahora; lo retomarás en la UD6.
4. ThreadLocalRandom: la mejor práctica moderna
4.1. El problema de Random en concurrencia
Random es seguro para usar desde varios hilos a la vez (es thread-safe), pero internamente usa una variable compartida protegida con sincronización. Si muchos hilos llaman a nextInt simultáneamente, hay contención y el rendimiento cae.
Para entender por qué, imagina una类比 simple:
Analogía del grifo compartido. Imagina un único grifo en un bar concurrido. Cada vez que alguien quiere agua, hace cola. El agua sale correctamente para todos, pero la cola hace que el proceso sea lento. Random es ese grifo: funciona, pero si 50 hilos quieren generar aleatorios a la vez, hacen cola.
4.2. La solución: ThreadLocalRandom
ThreadLocalRandom (introducido en Java 7) da a cada hilo su propio generador independiente. No hay sincronización ni contención: cada hilo tiene su propio "grifo".
Analogía del grifo por mesa. Imagina ahora que cada mesa del bar tiene su propio grifo. Los clientes de cada mesa no hacen cola: cada uno bebe de su grifo. Si hay 50 mesas, 50 personas pueden beber simultáneamente sin esperar.
4.3. Cómo se usa
A diferencia de Random, no se instancia con new. Se obtiene con el método estático ThreadLocalRandom.current():
4.4. Diferencias clave con Random
| Aspecto | Random |
ThreadLocalRandom |
|---|---|---|
| Creación | new Random() o new Random(semilla) |
ThreadLocalRandom.current() |
| Constructor con semilla | Sí, para reproducibilidad | No (no permite semilla explícita) |
| Concurrencia | Seguro pero con contención | Un generador por hilo, sin contención |
| Rendimiento en multihilo | Bajo | Alto |
| Rendimiento en un solo hilo | Bueno | Ligeramente mejor |
| Métodos disponibles | Casi idénticos | Casi idénticos, más sobrecargas |
| Recomendación | Para tests, depuración o cuando necesites reproducibilidad | Para producción, especialmente con múltiples hilos |
4.5. Cuándo usar cada uno
| Situación | Recomendación |
|---|---|
| Programa de un solo hilo, sin requisitos especiales | ThreadLocalRandom.current() (es ligeramente más rápido y la API es más cómoda) |
| Programa multihilo (servidores, juegos en red) | ThreadLocalRandom.current() (sin contención) |
| Tests automáticos donde necesitas reproducir una secuencia | new Random(semilla) |
| Depuración de un bug que depende de valores aleatorios | new Random(semilla) con la semilla que falló |
| Simulación científica reproducible | new Random(semilla) |
| Caso general sin requisitos | ThreadLocalRandom.current() |
Regla práctica para este curso: en ejercicios de clase donde necesites reproducibilidad (tests, ejemplos que tienen que dar la misma salida), usa new Random(semilla). En el resto, usa ThreadLocalRandom.current().
5. Caso especial: la clase SecureRandom
Existe una tercera clase para generar aleatorios: SecureRandom (en java.security). Genera aleatorios criptográficamente fuertes, lo que significa que son adecuados para tokens de seguridad, contraseñas, claves de cifrado, números de tarjeta, etc.
Por qué no usar Random para seguridad. Random y ThreadLocalRandom son predecibles: si un atacante conoce la semilla (por ejemplo, observando el reloj del sistema), puede reproducir toda la secuencia. SecureRandom usa fuentes del sistema operativo (ruido del hardware, movimientos del ratón, etc.) que son prácticamente imposibles de predecir.
No lo profundizamos aquí porque la criptografía está fuera del alcance de esta unidad, pero es importante que sepas que existe y que nunca uses Random o ThreadLocalRandom para nada relacionado con seguridad.
6. Diagrama de las clases de aleatorios en Java
classDiagram
class Random {
+Random()
+Random(long semilla)
+int nextInt()
+int nextInt(int bound)
+double nextDouble()
+boolean nextBoolean()
+long nextLong()
+double nextGaussian()
+IntStream ints(int n, int min, int max)
+void setSeed(long s)
}
class ThreadLocalRandom {
+static ThreadLocalRandom current()
+int nextInt(int min, int max)
+double nextDouble(double min, double max)
+boolean nextBoolean()
+long nextLong()
}
class SecureRandom {
+SecureRandom()
+int nextInt()
+byte[] generateSeed(int n)
}
Random <|-- ThreadLocalRandom
Random <|-- SecureRandom
note for ThreadLocalRandom "No se instancia con new.<br/>No admite semilla explicita.<br/>Un generador por hilo."
note for SecureRandom "Aleatorios criptograficamente fuertes.<br/>Usar SIEMPRE para seguridad."
note for Random "Para reproducibilidad, usar<br/>new Random(semilla)."
7. Ejemplo integrador
Programa que demuestra los conceptos de la sección: generadores con y sin semilla, métodos principales, y comparación con ThreadLocalRandom.
Salida (parcial; los valores sin semilla cambian entre ejecuciones):
Observa dos cosas importantes:
g2ayg2bproducen exactamente la misma secuencia porque comparten semilla. Esa es la reproducibilidad.- La distribución de 1000 tiradas no es exactamente uniforme (no sale 166-167 de cada cara), pero se acerca. Cuantas más tiradas, más se aproxima a la uniformidad teórica.
8. Errores comunes
| Error | Qué pasa | Cómo evitarlo |
|---|---|---|
Crear Random dentro de un bucle |
Cada iteración crea un generador nuevo, posiblemente con la misma semilla temporal → resultados repetidos | Crea el Random fuera del bucle, una sola vez |
Pasar bound <= 0 a nextInt(int) |
IllegalArgumentException |
Asegúrate de que bound sea positivo |
Confundir nextInt(bound) con nextInt(min, max) |
El segundo requiere Java 17+ y el segundo argumento es exclusivo | Recuerda: nextInt(6) da 0..5; nextInt(1, 7) da 1..6 |
Usar Random para seguridad |
Aleatorios predecibles, vulnerables a ataques | Usa SecureRandom para tokens, contraseñas, claves |
Instanciar ThreadLocalRandom con new |
Compila (es protected) pero no funciona como esperas |
Usa siempre ThreadLocalRandom.current() |
Olvidar el import |
"cannot find symbol" | Añade import java.util.Random; o import java.util.concurrent.ThreadLocalRandom; |
Esperar que nextDouble(5.0, 10.0) funcione en Random clásico |
Solo existe desde Java 17 y solo en sobrecargas con origen | Verifica @since en el Javadoc |
Pensar que nextGaussian() devuelve valores en [0, 1) |
Devuelve distribución normal con media 0 y desviación 1 (puede ser negativo o mayor que 1) | Recuerda: es la campana de Gauss, no un rango acotado |
9. Resumen
| Concepto | Idea clave |
|---|---|
| Pseudoaleatorio | Calculado por una fórmula determinista a partir de una semilla |
| Semilla | Valor inicial que determina toda la secuencia |
| Reproducibilidad | Misma semilla → misma secuencia |
Random() |
Generador con semilla basada en el reloj (no reproducible) |
Random(long semilla) |
Generador reproducible |
nextInt(int bound) |
Entero en [0, bound) (bound exclusivo) |
nextInt(int min, int max) |
Entero en [min, max) (Java 17+, max exclusivo) |
| Fórmula clásica de rango | g.nextInt(max - min + 1) + min para [min, max] |
nextDouble() |
Decimal en [0.0, 1.0) |
nextBoolean() |
true o false con probabilidad 1/2 |
nextGaussian() |
Distribución normal (media 0, desviación 1) |
ThreadLocalRandom.current() |
Mejor práctica moderna; un generador por hilo |
SecureRandom |
Para seguridad: contraseñas, tokens, claves |
10. Cuestionario de autoevaluación
Preguntas
-
¿Por qué se dice que los números generados por
Randomson "pseudoaleatorios" y no "verdaderamente aleatorios"? -
¿Qué es la semilla de un generador y por qué es importante?
-
Si creas dos objetos
Randomconnew Random(42), ¿qué relación hay entre las secuencias que producen? -
Escribe una línea que genere un entero entre 1 y 10 (ambos inclusive) usando
Random. -
Escribe la misma generación del ejercicio 4 usando la sobrecarga de Java 17+.
-
¿Qué rango de valores puede devolver
nextInt(6)(sin argumentos adicionales)? -
¿Qué devuelve
nextBoolean()? ¿YnextDouble()? ¿YnextGaussian()puede devolver valores negativos? -
¿Por qué
Randomtiene problemas de rendimiento cuando se usa desde muchos hilos? -
¿Cómo se obtiene una instancia de
ThreadLocalRandom? ¿Por qué no se usanew? -
¿Puedes ponerle una semilla a
ThreadLocalRandompara reproducir una secuencia? ¿Por qué? -
¿Qué clase usarías para generar un token de seguridad para restablecer una contraseña? ¿Por qué no
Random? -
¿Qué error obtienes si llamas a
nextInt(0)onextInt(-5)? -
Escribe las líneas para simular 100 tiradas de una moneda y contar cuántas "caras" (
true) salen. -
¿Cuál es la diferencia entre
nextInt(int bound)ynextInt(int origin, int bound)? -
Si en un test de JUnit necesitas generar datos aleatorios pero quieres que el test sea determinista (siempre dé la misma salida), ¿qué clase y qué constructor usarías?
Soluciones
-
Porque están calculados por una fórmula matemática determinista a partir de una semilla. La secuencia parece aleatoria (pasa tests estadísticos), pero si conoces la semilla puedes reproducirla exactamente.
-
La semilla es el valor inicial a partir del cual la fórmula genera toda la secuencia. Es importante porque determina completamente la secuencia: misma semilla, misma secuencia. Esto permite reproducibilidad en tests y depuración.
-
Producen exactamente la misma secuencia. Cada llamada a
nextInt,nextDouble, etc. devuelve los mismos valores en el mismo orden en ambos objetos. -
int n = g.nextInt(10) + 1;(dondeges unRandompreviamente creado). -
int n = g.nextInt(1, 11);(recordando que el segundo argumento es exclusivo). -
nextInt(6)devuelve un entero en[0, 6), es decir, 0, 1, 2, 3, 4 o 5. Nunca devuelve 6. -
nextBoolean()devuelvetrueofalsecon probabilidad 1/2 cada uno.nextDouble()devuelve undoubleen[0.0, 1.0). Sí,nextGaussian()puede devolver valores negativos: sigue una distribución normal con media 0 y desviación 1, así que aproximadamente la mitad de las veces devuelve un valor negativo. -
Porque
Randomusa internamente una variable compartida protegida con sincronización. Si muchos hilos llaman simultáneamente anextIntsobre el mismo objeto, deben hacer cola, lo que provoca contención y reduce el rendimiento. -
Con
ThreadLocalRandom.current(), método estático que devuelve una instancia única por hilo. No se usanewporque el constructor esprotected(acceso restringido) y porque la API está diseñada para dar a cada hilo su propio generador interno, no para crear generadores arbitrarios. -
No.
ThreadLocalRandomno permite establecer una semilla explícita. Es una decisión de diseño: como cada hilo tiene su propio generador inicializado internamente, no se garantiza reproducibilidad. Para reproducibilidad usanew Random(semilla). -
SecureRandom. PorqueRandomyThreadLocalRandomson predecibles: si un atacante conoce o adivina la semilla (por ejemplo, observando el reloj del sistema), puede reproducir toda la secuencia y predecir los tokens.SecureRandomusa fuentes del sistema operativo imposibles de predecir. -
IllegalArgumentException, porquebounddebe ser estrictamente positivo. -
Una solución:
-
nextInt(int bound)devuelve un entero en[0, bound).nextInt(int origin, int bound)(Java 17+) devuelve un entero en[origin, bound), permitiendo especificar el origen del rango, no solo el tamaño. -
new Random(semilla)con una semilla fija (por ejemplo,new Random(42)). Así cada ejecución del test genera los mismos datos y el resultado es determinista.
Evaluación del cuestionario
| Aciertos | Nivel |
|---|---|
| 14-15 | Dominas la sección. Adelante con la siguiente. |
| 10-13 | Bien, repasa los puntos fallados. |
| 6-9 | Repasa la sección, especialmente la diferencia entre Random y ThreadLocalRandom. |
| Menos de 6 | Vuelve a leer con calma y ejecuta el ejemplo integrador paso a paso en IntelliJ. |
11. Información ampliada y próximos pasos
- Javadoc oficial de
Random(Java 25): https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Random.html - Javadoc oficial de
ThreadLocalRandom: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/ThreadLocalRandom.html - Artículo "Random vs ThreadLocalRandom vs SecureRandom" (Baeldung): https://www.baeldung.com/java-thread-local-random
- Generadores de aleatorios modernos en Java 17+ (JEP 356): https://openjdk.org/jeps/356
Próximos pasos:
- Practica en IntelliJ. Copia el ejemplo integrador y ejecútalo varias veces. Comprueba que las secuencias con semilla son idénticas y las sin semilla cambian. Modifica el ejemplo del password para usar
ThreadLocalRandomen vez deRandom. - Experimenta con la simulación de dados. Cambia el número de tiradas (100, 1.000, 10.000, 100.000) y observa cómo la distribución se aproxima a la uniformidad teórica (1/6 ≈ 16,67% por cada cara).
- Avanza a la Sección 6 de UD2. Fechas y tiempos con
java.time: por quéjava.timereemplaza aDateyCalendar, las clasesLocalDate,LocalTime,LocalDateTime, los intervalosPeriodyDuration, y el formateo conDateTimeFormatter. Es una de las secciones más útiles del curso porque casi ninguna aplicación real se libra de trabajar con fechas.