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.Random is not suitable for security purposes like authentication tokens, password resets, or cryptography. For security, use java.security.SecureRandom on the JVM. kotlin.random.Random uses 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.Random is the primary choice for Kotlin code — multiplatform (JVM, JS, Native), thread-safe, and with a cleaner API than java.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 use Random.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 a MutableList.
  • .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 Random as a dependency so code using random is easy to test — pass Random(seed) in tests, Random.Default for production.
  • kotlin.random.Random isn’t cryptographically secure — use java.security.SecureRandom for 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).

← Previous: Comparator & Sorting   Next: Duration →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact