JavaRush /Курси /Kotlin SELF /Random — генерація випадкових Int і Double

Random — генерація випадкових Int і Double

Kotlin SELF
Рівень 6 , Лекція 4
Відкрита

1. Випадкові числа в програмах

Випадкові числа — чудовий спосіб зробити застосунок менш «деревʼяним». Коли все заздалегідь передбачувано, ви швидко впираєтеся в стелю: гра завжди однакова, тести нудні, симуляція не схожа на життя. Random дає змогу додати варіативності: згенерувати кидок кубика, випадкову знижку, імовірність події, лутбокси або «випадковий» сценарій. І так, це той самий момент, коли компʼютер раптом удає, ніби він не детермінована машина, а трішки чарівник.

Важливо одразу домовитися про дві речі. По‑перше, у програмуванні «випадковість» зазвичай означає псевдовипадковість: числа виглядають випадковими, але всередині працює хитрий алгоритм. По‑друге, вам потрібні правила меж. Інакше ви постійно ловитимете помилку «на 1» (найприкріша з помилок, бо мозок запевняє: «ну ж усе правильно!»).

kotlin.random.Random: джерело «випадковості»

У Kotlin стандартний генератор псевдовипадкових чисел міститься в пакеті kotlin.random. Зазвичай ви використовуєте обʼєкт Random і викликаєте в нього методи на кшталт nextInt() або nextDouble().

Почнімо з найпростішого: Random — це як «коробка з кульками». Ви просите: «Дай мені кульку з числом», і він видає наступну. Але щоб вам було простіше, у методів є контракти: які числа можуть випасти, а які — ні.

Мініприклад: просто отримати «якесь» ціле число.

import kotlin.random.Random

fun main() {
    val x = Random.nextInt()
    println("x = $x") // x = (якесь ціле, кожен запуск різне)
}

Тут важливо не очікувати «гарних» значень. Може бути і відʼємне, і велике, і маленьке. Якщо вам потрібен певний діапазон (а він вам майже завжди потрібен), задавайте межі.

2. Random.nextInt(from, until): головне правило про межі

Коли ви хочете число «в межах від А до Б», зазвичай використовують: Random.nextInt(from, until)

І саме тут заховане ключове правило: from включається, а until не включається.

Тобто результат лежить у діапазоні: from .. (until - 1)

Це правило — не «примха розробників Kotlin», а дуже практична домовленість. Так зручно працювати з діапазонами, розмірами та індексами (згодом ви це особливо відчуєте). У прикладах документації Kotlin це добре видно на класичному «кидаємо кубик, доки не випаде 6»: там використовується Random.nextInt(1, 7). Сімка якраз вибрана так, щоб 6 була досяжною.

Зафіксуймо це невеликою таблицею, щоб було легше запамʼятати:

Виклик Можливі результати
Random.nextInt(0, 10)
0..9
Random.nextInt(1, 7)
1..6
Random.nextInt(-3, 3)
-3..2

Зверніть увагу: останній рядок особливо «підступний». Якщо ви очікували -3..3, то ви вже потрапили в класичну пастку.

3. Помилка «на +1»: чому кубик — найкращий учитель

У кожної теми в програмуванні є свій підводний камінь. У циклів — нескінченний while, у рядків — забутий пробіл наприкінці, у null — смуток і філософія. А для Random таким символом стає кубик d6 (1..6): він ідеально показує проблему меж.

Зараз ми зробимо невеличкий шматочок консольного застосунку. Нехай це буде тренувальна програма «LuckyCafe»: вона видаватиме клієнту випадкову знижку або бонус — просто щоб нам було що розвивати.

Спочатку реалізуймо «кидок кубика» правильно:

import kotlin.random.Random

fun main() {
    val dice = Random.nextInt(from = 1, until = 7)
    println("Випало: $dice") // Випало: (1..6)
}

Чому until = 7? Тому що верхня межа не включається. Так ми й отримуємо 1..6.

А тепер — неправильний варіант, який дуже часто пишуть новачки (і інколи не лише новачки, але потім роблять вигляд, що «так і задумано»):

import kotlin.random.Random

fun main() {
    val dice = Random.nextInt(from = 1, until = 6)
    println("Випало: $dice") // Випало: (1..5) — шістка НІКОЛИ не випаде
}

Це не «рідко випадає 6». Це «ніколи». Така помилка може жити в проєкті тижнями, бо звучить правдоподібно: «Ну так, 6 просто поки не випадала». Тож корисна звичка така: якщо ви робите діапазон «включно», подумки промовляйте формулу until = max + 1.

4. Double-випадковість

Random.nextDouble(): випадкове число від 0 до 1

Якщо nextInt() — це «випадкове ціле», то nextDouble() найчастіше сприймають як «випадкову частку». За замовчуванням:

Random.nextDouble() видає дійсне число в діапазоні від 0.0 до 1.0 (одиниця при цьому зазвичай не вважається гарантовано досяжною, але в побутовому сенсі думайте «майже до 1.0»).

Практично це зручно як «імовірність» або «коефіцієнт». Наприклад: «з імовірністю 20 % видаємо бонус» або «випадково трохи змінюємо ціну чи вагу».

Приклад: згенеруємо «коефіцієнт удачі» як число від 0.0 до 1.0:

import kotlin.random.Random

fun main() {
    val luck = Random.nextDouble()
    println("Удача = $luck") // Удача = 0.483920...
}

Майже завжди це буде довгий дріб. Це нормально: компʼютер не зобовʼязаний дбати про красу виводу — це вже наша робота.

Масштабування випадкового дробу: робимо «0..1» корисним

Сам по собі дріб 0..1 не завжди зручний. Частіше хочеться «випадкове число в діапазоні». Наприклад: знижку 5%..25% або випадкову ціну 100.0..150.0.

Є два базові шляхи, і обидва корисні. Почнемо з найзрозумілішого «математичного» способу: беремо випадкове число й «розтягуємо» його на потрібний діапазон.

Формула така: value = min + Random.nextDouble() * (max - min)

Приклад: випадкова знижка від 5% до 25% (тобто 0.05..0.25 як діапазон):

import kotlin.random.Random

fun main() {
    val min = 0.05
    val max = 0.25
    val discount = min + Random.nextDouble() * (max - min)

    println("Знижка = $discount") // Знижка = 0.1372...
}

Так, це знову довгий дріб. Людині приємніше бачити відсотки, тож додамо вивід у відсотках.

Random + округлення для виводу: робимо результат людським

Випадкові значення майже завжди виходять «некрасивими», наприклад: 0.1372049182. Це чудове число для математика й жахливе для користувача. Тож нормальний патерн такий: згенерували → масштабували → округлили для виводу.

Ми не заглиблюватимемося в нові правила округлення, але покажемо типовий прийом: округлення до 1 знака після коми через round.

import kotlin.math.round
import kotlin.random.Random

fun main() {
    val discount = 0.05 + Random.nextDouble() * 0.20 // 0.05..0.25
    val percent = discount * 100.0

    val prettyPercent = round(percent * 10.0) / 10.0
    println("Знижка = $prettyPercent%") // Знижка = 13.7%
}

І ось тепер наш «LuckyCafe» починає виглядати як застосунок, а не як протокол вимірювань із космічного корабля.

5. Мініприклад: «LuckyCafe» — бонус до замовлення

Зараз ми зберемо невеликий проєкт, який використовує і nextInt(...), і nextDouble(), і головне правило меж. Сценарій такий: ми друкуємо чек «кавʼярні», а потім випадково визначаємо бонус дня: або знижка, або безкоштовний сироп, або «на жаль, сьогодні без бонусів». Кавʼярня теж має вміти казати «ні», інакше розориться.

Випадковий вибір варіанта через nextInt(0, 3)

Коли потрібно вибрати один із кількох варіантів, зручно генерувати число-індекс: 0, 1, 2. І саме тут правило until знову відіграє роль. Якщо варіантів 3, то until має бути 3, бо результат буде 0..2.

import kotlin.random.Random

fun main() {
    val bonusCase = Random.nextInt(from = 0, until = 3)

    println("bonusCase = $bonusCase") // bonusCase = 0/1/2
}

Тут уже можна «навісити» логіку на if/else (ми це вміємо). Так, пізніше буде when. Але поки що ми граємося тими кубиками, які вам видали на попередніх уроках.

Перетворюємо номер варіанта на текст через if/else

Тепер зробімо наш перший вивід:

import kotlin.random.Random

fun main() {
    val bonusCase = Random.nextInt(from = 0, until = 3)

    val message = if (bonusCase == 0) {
        "Бонус: знижка на замовлення"
    } else if (bonusCase == 1) {
        "Бонус: безкоштовний сироп"
    } else {
        "Бонус: сьогодні без бонусів (але ви все одно молодці)"
    }

    println(message)
}

Зверніть увагу: ми спеціально використовуємо 0..2, бо так зручніше, ніж 1..3. Індекси та «відлік з нуля» спочатку дратують, а потім стають як рідні.

Якщо випала знижка — генеруємо її через nextDouble()

Тепер зробімо знижку не просто текстом, а числом. Припустімо, знижка від 5% до 20% (це діапазон 0.05..0.20).

import kotlin.math.round
import kotlin.random.Random

fun main() {
    val bonusCase = Random.nextInt(from = 0, until = 3)

    if (bonusCase == 0) {
        val discount = 0.05 + Random.nextDouble() * 0.15
        val percent = round(discount * 1000.0) / 10.0
        println("Бонус: знижка $percent%") // Бонус: знижка 12.3%
    } else if (bonusCase == 1) {
        println("Бонус: безкоштовний сироп")
    } else {
        println("Бонус: сьогодні без бонусів")
    }
}

Ми округлили відсоток до 1 знака після коми. Можна й до цілого — залежить від задачі. Тут важливо побачити звʼязку: nextDouble() → масштабування → округлення.

Мінігра всередині: «кидаємо кубик, доки не випаде 6»

Інколи випадковість використовують не для бонусів, а для повторюваних спроб — доки не станеться подія. В офіційних прикладах Kotlin часто показують ідею «кидаємо кубик до шістки» через do-while і Random.nextInt(1, 7).

Ми додамо це як «рекламну акцію»: бариста кидає кубик, і якщо випала 6 — видаємо додатковий бонус.

import kotlin.random.Random

fun main() {
    var roll: Int

    do {
        roll = Random.nextInt(1, 7)
        println("Бариста кинув кубик: $roll") // Бариста кинув кубик: 2
    } while (roll != 6)

    println("Випала 6! Додаємо печиво до замовлення!") // Випала 6! ...
}

6. Шпаргалка щодо until

З Random.nextInt(from, until) ви майже завжди замислюєтеся в одному й тому самому місці: «А чи включається верхня межа?». Тому корисно тримати в голові «шпаргалку-формулу» і звіряти з нею будь-яку генерацію.

Якщо вам потрібно:

  • від a до b включно, ви пишете nextInt(a, b + 1).
  • від 0 до (n - 1) (наприклад індекс), ви пишете nextInt(0, n).

Саме тому часто кажуть: «until — це як size». Якщо у вас 10 варіантів, until = 10, і ви отримаєте 0..9.

До речі, таке саме мислення видно й в інших частинах стандартної бібліотеки: багато операцій спираються на той самий принцип «верхня межа не включається», бо так простіше не виходити за межі.

7. Типові помилки під час роботи з Random

Помилка №1: переплутати межі й отримати «ніколи не випадає».
Найчастіша пастка — написати Random.nextInt(1, 6) і чекати 1..6. Але за контрактом верхня межа не включається, тож вийде 1..5. Це ламає статистику, ігрову механіку та віру в справедливість світу. Лікується однією звичкою: якщо вам потрібно «до b включно», використовуйте until = b + 1. На прикладі з кубиком це добре видно: Random.nextInt(1, 7) дає 1..6.

Помилка №2: очікувати «гарних» чисел від nextDouble().
Random.nextDouble() майже завжди дає довгий дріб — і це нормально. Помилка — показувати його користувачу «як є» і дивуватися, що застосунок виглядає дивно. Правильний стиль: або переводити в відсотки, або округлювати до потрібної кількості знаків (як ми зробили через round).

Помилка №3: змішувати «імовірність» і «відсотки» без перетворення.
Дуже поширена плутанина: ви отримали p = Random.nextDouble(), вирішили, що це «відсотки», і друкуєте «знижка 0.42%». Насправді це 42 % як частка (0.42), і для відсотків потрібно множити на 100. І навпаки: якщо ви зберігаєте відсотки як 42, а потім використовуєте як імовірність, логіка розвалюється. Домовтеся із собою: або ви зберігаєте частку (0.0..1.0), або відсотки (0..100). І щоразу явно переводьте одне в інше.

Помилка №4: думати, що Random — «магічна справжня випадковість».
У навчальних задачах це майже ніколи не важливо, але корисно памʼятати: Random видає псевдовипадкові числа. Це означає, що для ігор, симуляцій і навчальних прикладів він ідеальний, а для криптографії та безпеки потрібні інші підходи (але це взагалі не тема цього дня — просто тримаймо це в голові, щоб не будувати «генератор паролів банку» на nextInt()).

Помилка №5: робити діапазон через «складну формулу», хоча можна простіше.
Новачки інколи пишуть щось на кшталт a + Random.nextInt() % (b - a) і отримують відʼємні числа, дивний розподіл і багато болю. Якщо вам потрібен діапазон — використовуйте nextInt(from, until) і живіть спокійно: він якраз створений, щоб не винаходити велосипед (а потім лагодити його, бо в нього квадратні колеса).

Помилка №6: забути, що until — це «як розмір», і зламати вибір із варіантів.
Коли ви обираєте варіант із трьох (0, 1, 2), потрібно nextInt(0, 3). Якщо написати nextInt(0, 2), третій варіант ніколи не випаде. У маленькому прикладі це помітно одразу, а у великому застосунку може проявитися як «Чому користувачі ніколи не отримують бонус №3?». І тоді ви витратите пів дня, щоб зрозуміти: ви просто «відрізали» гілку випадковості.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ