Random #
Random numbers are a more common need than you’d think — creating dummy data for testing, randomizing display order, picking promo items randomly, Monte Carlo simulations, game mechanics, security tokens, and much more. Kotlin ships with kotlin.random.Random, a modern and multiplatform API — unlike java.util.Random which is only available on the JVM. kotlin.random.Random works on Kotlin/JVM, Kotlin/JS, and Kotlin/Native without code changes. This article covers the entire Kotlin Random API: from basic usage, seed-based reproducibility control, collection shuffling, sampling, to idiomatic patterns for testing and simulation.
kotlin.random.Random vs java.util.Random #
flowchart LR
A["Need random numbers\nin Kotlin"] --> B{Platform?}
B --> C["JVM only\njava.util.Random\njava.util.ThreadLocalRandom\njava.security.SecureRandom"]
B --> D["Multiplatform\nkotlin.random.Random\nRuns on JVM, JS, Native"]
D --> E["Recommended\nfor new code"]
C --> F["Use only if you need\na specific Java API\nor SecureRandom"]// The Java way — JVM only
import java.util.Random
val javaRandom = Random()
val n1 = javaRandom.nextInt(100)
// The Kotlin way — multiplatform, cleaner API
import kotlin.random.Random
val n2 = Random.nextInt(100) // 0 to 99 (exclusive)
val n3 = Random.nextInt(1, 101) // 1 to 100 (inclusive)
val d = Random.nextDouble() // 0.0 to less than 1.0
val b = Random.nextBoolean() // true or false
// Random.Default is a global thread-safe instance
// Can be used directly without creating a new instance
println(Random.nextInt(10)) // 0..9
// Or create your own instance
val rng = Random(seed = 42)
println(rng.nextInt(10))
Basic Functions #
nextInt — Random Integers #
// nextInt() without arguments — the entire Int range
val acakPenuh: Int = Random.nextInt()
// nextInt(bound) — 0 to bound-1 (upper exclusive)
val nol_sampai_9: Int = Random.nextInt(10) // 0, 1, 2, ..., 9
val nol_sampai_99: Int = Random.nextInt(100) // 0, 1, 2, ..., 99
// nextInt(from, until) — an explicit range
val satu_sampai_6: Int = Random.nextInt(1, 7) // a die: 1, 2, 3, 4, 5, 6
val negatif: Int = Random.nextInt(-10, 10) // -10 to 9
// Use an IntRange
val dariRange: Int = Random.nextInt(1..6) // more expressive
val dariRange2: Int = (1..6).random() // a shortcut on a range
// ANTI-PATTERN: a manual implementation prone to off-by-one errors
val salah = (Math.random() * 6).toInt() + 1 // depends on java.lang.Math
// CORRECT: use nextInt with a clear range
val dadu = Random.nextInt(1, 7) // 1 to 6 inclusive
nextDouble and nextFloat #
// nextDouble() — 0.0 (inclusive) to 1.0 (exclusive)
val prob: Double = Random.nextDouble() // uniform distribution [0.0, 1.0)
// nextDouble(from, until) — a custom range
val suhu: Double = Random.nextDouble(36.0, 38.0) // 36.0 to less than 38.0
val lon: Double = Random.nextDouble(95.0, 141.0) // Indonesian longitude
val lat: Double = Random.nextDouble(-11.0, 6.0) // Indonesian latitude
// nextFloat — lower precision, suitable for graphics
val f: Float = Random.nextFloat() // [0.0f, 1.0f)
// Generation in specific distributions
// Normal (Gaussian) distribution — not in the stdlib, but easy to make
fun nextGaussian(mean: Double = 0.0, std: Double = 1.0): Double {
// The Box-Muller transform
val u1 = Random.nextDouble()
val u2 = Random.nextDouble()
val z0 = kotlin.math.sqrt(-2.0 * kotlin.math.ln(u1)) *
kotlin.math.cos(2.0 * kotlin.math.PI * u2)
return mean + std * z0
}
// Exponential distribution
fun nextExponential(lambda: Double = 1.0): Double =
-kotlin.math.ln(1.0 - Random.nextDouble()) / lambda
nextBoolean and nextBits #
// nextBoolean — 50/50 true/false
val koinLempar: Boolean = Random.nextBoolean() // true or false, equal odds
// Non-50/50 odds
fun acakDenganPeluang(peluang: Double): Boolean {
require(peluang in 0.0..1.0) { "Probability must be 0.0 to 1.0" }
return Random.nextDouble() < peluang
}
val menang = acakDenganPeluang(0.30) // 30% chance of true
val hujan = acakDenganPeluang(0.70) // 70% chance of true
// nextBits — take n random bits (useful for bit-level operations)
val bits: Int = Random.nextBits(8) // values 0-255
// nextLong — random Long values
val id: Long = Random.nextLong()
val idRange: Long = Random.nextLong(1_000_000L, 9_999_999L) // a 7-digit ID
nextBytes — Random Byte Arrays #
// nextBytes — fill an array with random bytes
val buffer = ByteArray(16)
Random.nextBytes(buffer)
println(buffer.toHexString()) // a hex dump of 16 random bytes
// Or create a new array directly
val tokenBytes: ByteArray = Random.nextBytes(32) // 256 bits for a token
// Convert to a hex string
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
// Generate a simple token (NOT for production security — use SecureRandom)
fun buatToken(panjangByte: Int = 16): String =
Random.nextBytes(panjangByte).toHexString()
println(buatToken()) // "a3f8c2d1e7b4..." (32 hex characters from 16 bytes)
kotlin.random.Randomis not suitable for security purposes like authentication tokens, password resets, or cryptography. For security, usejava.security.SecureRandomon the JVM.kotlin.random.Randomuses a PRNG (Pseudo-Random Number Generator) that’s deterministic and predictable if the seed is known.
Seeds — Reproducibility #
A seed is the initial value that determines the entire sequence of random numbers to be generated. Random instances with the same seed always produce the same sequence — very useful for testing, debugging, and simulations that need to be replicated.
// Without a seed — different every run
val r1 = Random
println(r1.nextInt(100)) // a different result every run
// With a seed — the same every run
val seeded = Random(seed = 42)
println(seeded.nextInt(100)) // always produces the same value
println(seeded.nextInt(100)) // the next value is also deterministic
// Two instances with the same seed produce identical sequences
val a = Random(42)
val b = Random(42)
val listA = List(5) { a.nextInt(100) }
val listB = List(5) { b.nextInt(100) }
println(listA == listB) // true — exactly the same
// IMPORTANT: a seed makes the SEQUENCE deterministic, not just one value
val c = Random(42)
repeat(3) { println(c.nextInt(100)) }
// Output is always the same: e.g., 33, 1, 76
// Applying seeds for testing
class GameSimulasi(private val rng: Random = Random.Default) {
fun lemparDadu(): Int = rng.nextInt(1, 7)
fun acakPosisi(): Pair<Int, Int> = rng.nextInt(0, 10) to rng.nextInt(0, 10)
}
// In tests — seed for reproduction
@Test
fun `simulation must be deterministic with a seed`() {
val rngTetap = Random(seed = 12345)
val game = GameSimulasi(rngTetap)
val hasilDadu = game.lemparDadu()
val posisi = game.acakPosisi()
// With the same seed, the results are always the same — the test isn't flaky
assertEquals(hasilDadu, GameSimulasi(Random(12345)).lemparDadu())
}
Collection Shuffling #
shuffle — Randomizing Order #
// shuffle — in-place, only for MutableList
val kartu = (1..52).toMutableList()
kartu.shuffle() // random order in-place
kartu.shuffle(Random(42)) // with a seed for reproducibility
// shuffled — produces a new List (doesn't change the original)
val daftar = listOf("A", "B", "C", "D", "E")
val teracak: List<String> = daftar.shuffled()
val teracakSeed: List<String> = daftar.shuffled(Random(99))
println(daftar) // [A, B, C, D, E] — unchanged
println(teracak) // [C, A, E, B, D] — random order
random — Taking a Random Element #
// random() — take one random element
val buah = listOf("apel", "jeruk", "mangga", "durian", "jambu")
val acak: String = buah.random()
val acakSeed: String = buah.random(Random(42))
// randomOrNull — safe for empty lists
val kosong = emptyList<String>()
val hasilNull: String? = kosong.randomOrNull() // null, doesn't throw
val hasilNormal: String? = buah.randomOrNull() // one of the fruits
// ANTI-PATTERN: manual random index access
val salah = buah[Random.nextInt(buah.size)] // can throw if the list is empty
// CORRECT: use .random() or .randomOrNull()
val benar = buah.randomOrNull() ?: "no fruits"
// random() on Ranges
val angkaAcak = (1..100).random() // a number between 1 and 100
val hurufAcak = ('a'..'z').random() // a random lowercase letter
Sampling — Taking Several Elements #
// Kotlin doesn't have a built-in sampling function — but it's easy to make
// Sampling without replacement (each element can only be picked once)
fun <T> List<T>.sampel(n: Int, rng: Random = Random): List<T> {
require(n <= size) { "Can't take $n samples from a list of size $size" }
return shuffled(rng).take(n)
}
val peserta = listOf("Andi", "Budi", "Clara", "Dina", "Eva", "Fani", "Gani")
val pemenang3 = peserta.sampel(3)
println(pemenang3) // 3 random names without duplicates
// Sampling with replacement (elements can be picked more than once)
fun <T> List<T>.sampelDenganPengembalian(n: Int, rng: Random = Random): List<T> =
List(n) { random(rng) }
val dadux5 = (1..6).toList().sampelDenganPengembalian(5)
// E.g., [3, 6, 3, 1, 5] — numbers can appear more than once
// Weighted random — pick based on weights
fun <T> pilihBerbobot(pilihan: List<Pair<T, Double>>, rng: Random = Random): T {
val totalBobot = pilihan.sumOf { it.second }
val acak = rng.nextDouble() * totalBobot
var kumulatif = 0.0
for ((item, bobot) in pilihan) {
kumulatif += bobot
if (acak < kumulatif) return item
}
return pilihan.last().first
}
val loot = listOf(
"Common Sword" to 0.60,
"Rare Sword" to 0.25,
"Epic Sword" to 0.10,
"Legendary Sword" to 0.05
)
val item = pilihBerbobot(loot) // 60% a Common Sword, 5% a Legendary
Data Generation for Testing #
One of Random’s most valuable uses is creating realistic dummy data for testing.
// A deterministic data generator with a seed
class DataGenerator(seed: Long = System.currentTimeMillis()) {
private val rng = Random(seed)
private val namaDepan = listOf("Andi", "Budi", "Clara", "Dina", "Eva",
"Fani", "Gani", "Hana", "Indra", "Joko")
private val namaBelakang = listOf("Santoso", "Wijaya", "Kusuma", "Pratama",
"Hidayat", "Nugroho", "Saputra", "Utama")
private val departemen = listOf("Engineering", "Marketing", "HR", "Finance", "Operations")
private val kota = listOf("Jakarta", "Bandung", "Surabaya", "Medan", "Makassar")
fun nama(): String =
"${namaDepan.random(rng)} ${namaBelakang.random(rng)}"
fun email(nama: String): String =
"${nama.lowercase().replace(" ", ".")}${rng.nextInt(100)}@example.com"
fun gaji(): Double =
rng.nextDouble(8_000_000.0, 25_000_000.0).let {
(it / 500_000).toLong() * 500_000.0 // round to 500k
}
fun usia(): Int = rng.nextInt(22, 58)
fun karyawan(): Map<String, Any> {
val nama = nama()
return mapOf(
"nama" to nama,
"email" to email(nama),
"departemen" to departemen.random(rng),
"gaji" to gaji(),
"usia" to usia(),
"kota" to kota.random(rng)
)
}
fun daftarKaryawan(n: Int): List<Map<String, Any>> =
List(n) { karyawan() }
}
// Usage in tests — the same seed produces the same data
val gen = DataGenerator(seed = 42L)
val karyawan100 = gen.daftarKaryawan(100)
// Usage in development — a random seed for variety
val genDev = DataGenerator()
val dataDev = genDev.daftarKaryawan(50)
Simulations with Random #
// Monte Carlo simulation — estimating the value of Pi
fun estimasiPi(iterasi: Int, seed: Long = 42L): Double {
val rng = Random(seed)
var dalamLingkaran = 0
repeat(iterasi) {
val x = rng.nextDouble(-1.0, 1.0)
val y = rng.nextDouble(-1.0, 1.0)
if (x * x + y * y <= 1.0) dalamLingkaran++
}
return 4.0 * dalamLingkaran / iterasi
}
println(estimasiPi(1_000)) // ~3.14 (rough)
println(estimasiPi(1_000_000)) // ~3.14159 (more accurate)
println(estimasiPi(10_000_000)) // ~3.141592 (very accurate)
// A random walk simulation
data class Posisi(val x: Int, val y: Int)
fun randomWalk(langkah: Int, seed: Long = 42L): List<Posisi> {
val rng = Random(seed)
val arah = listOf(
Posisi(0, 1), // up
Posisi(0, -1), // down
Posisi(1, 0), // right
Posisi(-1, 0) // left
)
val jejak = mutableListOf(Posisi(0, 0))
repeat(langkah) {
val sekarang = jejak.last()
val gerak = arah.random(rng)
jejak.add(Posisi(sekarang.x + gerak.x, sekarang.y + gerak.y))
}
return jejak
}
val jejak = randomWalk(100)
println("Final position: ${jejak.last()}")
UUIDs and Unique IDs #
// UUID using java.util.UUID (JVM only)
import java.util.UUID
val uuid = UUID.randomUUID().toString()
// "550e8400-e29b-41d4-a716-446655440000"
// A unique numeric ID with timestamp + random
fun buatIdUnik(): Long {
val timestamp = System.currentTimeMillis()
val random = Random.nextLong(0, 999_999)
return timestamp * 1_000_000 + random
}
// A human-readable reference code
fun buatKodeReferensi(panjang: Int = 8): String {
val karakter = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // no ambiguous characters
return buildString {
repeat(panjang) { append(karakter.random()) }
}
}
println(buatKodeReferensi()) // e.g., "K7MPQR2X"
println(buatKodeReferensi(12)) // e.g., "3HMKPQ7RVXZT"
// A 6-digit OTP (One-Time Password) code
fun buatOTP(): String = Random.nextInt(100_000, 999_999).toString()
println(buatOTP()) // "847392"
Idiomatic Patterns #
Dependency Injection for Testability #
// ANTI-PATTERN: hard-coded Random.Default — hard to test
class RekomendatorProduk(val katalog: List<Produk>) {
fun rekomendasikan(): Produk = katalog.random() // can't be controlled in tests
}
// CORRECT: inject Random as a dependency
class RekomendatorProduk(
val katalog: List<Produk>,
private val rng: Random = Random.Default
) {
fun rekomendasikan(): Produk = katalog.random(rng)
fun rekomendasikan(n: Int): List<Produk> = katalog.shuffled(rng).take(n)
}
// Test with a deterministic seed
@Test
fun `recommendations must be consistent with a seed`() {
val katalog = listOf(Produk("A", 1.0, 1), Produk("B", 2.0, 1), Produk("C", 3.0, 1))
val reko = RekomendatorProduk(katalog, Random(42))
val hasil1 = reko.rekomendasikan()
val rekoSama = RekomendatorProduk(katalog, Random(42))
val hasil2 = rekoSama.rekomendasikan()
assertEquals(hasil1, hasil2) // always the same with the same seed
}
Exponential Backoff with Jitter #
// Retry with exponential backoff + random jitter
// Jitter prevents a thundering herd — all clients retrying at once
fun hitungJeda(percobaan: Int, jedaBase: Long = 1000L, maxJeda: Long = 30_000L): Long {
val eksponensial = jedaBase * (1L shl percobaan.coerceAtMost(10))
val jitter = Random.nextLong(0, eksponensial / 2) // ±50% jitter
return (eksponensial + jitter).coerceAtMost(maxJeda)
}
// Attempt 0: ~1000ms + jitter 0-500ms
// Attempt 1: ~2000ms + jitter 0-1000ms
// Attempt 2: ~4000ms + jitter 0-2000ms
// ...
suspend fun retryDenganJitter(
maxPercobaan: Int = 3,
aksi: suspend () -> Unit
) {
repeat(maxPercobaan) { percobaan ->
try {
aksi()
return
} catch (e: Exception) {
if (percobaan == maxPercobaan - 1) throw e
val jeda = hitungJeda(percobaan)
kotlinx.coroutines.delay(jeda)
}
}
}
Summary #
kotlin.random.Randomis the primary choice for Kotlin code — multiplatform (JVM, JS, Native), thread-safe, and with a cleaner API thanjava.util.Random.Random.nextInt(from, until)for random integers within a range.(1..6).random()is an idiomatic shortcut directly on a range.Random.nextDouble()produces a uniform value [0.0, 1.0). For a custom range useRandom.nextDouble(from, until).- Seeds make random number sequences deterministic — instances with the same seed produce identical sequences. Use seeds for reproducible tests and replicable simulations.
shuffled()produces a new List with a random order without changing the original.shuffle()shuffles in-place and can only be called on aMutableList..random()for taking one random element from a collection..randomOrNull()for safe handling of collections that might be empty.- Weighted random isn’t built-in — implement it by calculating the total weight, generating a random number within the total range, then picking the item based on cumulative weight.
- Inject
Randomas a dependency so code using random is easy to test — passRandom(seed)in tests,Random.Defaultfor production.kotlin.random.Randomisn’t cryptographically secure — usejava.security.SecureRandomfor security tokens, session IDs, and other cryptographic needs.- Exponential backoff with jitter is an important pattern for retry logic in distributed systems — jitter prevents all clients from retrying simultaneously (a thundering herd).