Генерация случайных чисел в Android на Kotlin: что под капотом?

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как заставить Android «бросать кости»? Разбираем генерацию случайных чисел на Kotlin

Вы когда-нибудь задумывались, как ваш телефон «выбирает» случайное число? Вроде бы простая задача — получить случайное целое от 5 до 10. Но под капотом Android и Kotlin скрывается целый механизм, от которого зависит не только удобство, но и безопасность вашего приложения. Неудивительно, что вопрос «How can I generate random number in specific range in Android?» собрал тысячи просмотров на Stack Overflow. Мы не просто дадим готовый ответ — мы заглянем в исходники и разберемся, что происходит на самом деле.

В этой статье вы узнаете:

  • Как правильно генерировать случайное число в заданном диапазоне на Kotlin.
  • Чем отличаются kotlin.random.Random, java.util.Random и SecureRandom.
  • Почему для азартных игр нужен один класс, а для генерации паролей — другой.
  • Как избежать типичных ошибок, которые превращают «случайность» в предсказуемость.

Погнали!

Классика жанра: Random в Kotlin (и почему это не всегда правильно)

Самый очевидный способ — использовать kotlin.random.Random. Он встроен прямо в язык и работает из коробки. Вот как выглядит генерация случайного числа от 0 до 100 (включительно):

import kotlin.random.Random

fun main() {
    val randomNumber = Random.nextInt(0, 101) // от 0 до 100 включительно
    println("Случайное число: $randomNumber")
}

Всё просто. Но давайте заглянем под капот. Random.nextInt(from, until) внутри использует java.util.Random (на платформе JVM). А тот, в свою очередь, генерирует числа с помощью линейного конгруэнтного генератора (LCG). Формула простая:

new_seed = (old_seed * A + C) mod M

Где A, C и M — константы. Это быстро, но не криптографически стойко. Если злоумышленник узнает хотя бы несколько чисел, он может восстановить seed и предсказать все последующие.

Когда использовать: Для игр (не на деньги), случайного выбора элемента из списка, перемешивания карточек в обычном приложении.

Продвинутый уровень: ThreadLocalRandom для многопоточности

Если ваше приложение активно использует корутины или многопоточность, обычный Random может стать узким местом. Каждый вызов nextInt() синхронизируется, чтобы избежать состояния гонки. Решение — ThreadLocalRandom из Java:

import java.util.concurrent.ThreadLocalRandom

fun getRandomInRange(min: Int, max: Int): Int {
    return ThreadLocalRandom.current().nextInt(min, max + 1)
}

Этот класс выделяет отдельный экземпляр Random для каждого потока. Синхронизация не нужна — каждый поток работает со своим генератором. На практике это даёт прирост производительности в многопоточных сценариях.

Безопасность превыше всего: SecureRandom

Теперь представьте, что вы генерируете одноразовый пароль (OTP) или токен для восстановления доступа. Использовать обычный Random — преступление. В дело вступает SecureRandom:

import java.security.SecureRandom

fun generateSecureCode(length: Int): String {
    val secureRandom = SecureRandom()
    val digits = "0123456789"
    return (1..length)
        .map { digits[secureRandom.nextInt(digits.length)] }
        .joinToString("")
}

SecureRandom собирает энтропию из источников операционной системы: шумы с микрофона, тайминги нажатий клавиш (в эмуляторе — из файла /dev/random или /dev/urandom). Это делает его непредсказуемым. Но за это приходится платить скоростью — он может быть в десятки раз медленнее обычного Random.

Важно: На Android SecureRandom по умолчанию использует провайдер AndroidOpenSSL, который оптимизирован для мобильных устройств. Не пытайтесь создать свой генератор «ещё более случайным» — доверьтесь системе.

Генерация случайного числа в диапазоне: пошаговый разбор

Вернёмся к главному вопросу. Допустим, вам нужно получить число от min до max включительно. Самый надёжный способ на Kotlin (без сторонних библиотек):

import kotlin.random.Random

fun IntRange.random(): Int {
    return Random.nextInt(this.first, this.last + 1)
}

// Использование
val diceRoll = (1..6).random() // от 1 до 6
println("Бросок кубика: $diceRoll")

Это расширение для IntRange. Оно чистое, читаемое и использует встроенный Random. Если вам нужен криптостойкий вариант — замените Random на SecureRandom.

Подводные камни: что может пойти не так?

  • Ошибка на единицу: Random.nextInt(0, 100) даст числа от 0 до 99. Чтобы получить до 100 включительно, пишите Random.nextInt(0, 101).
  • Пересоздание Random: Не делайте val r = Random() каждый раз. Создайте один экземпляр и переиспользуйте — иначе вы рискуете получить повторяющиеся последовательности (особенно если создавать их быстро).
  • Seed по умолчанию: Random() инициализируется текущим временем в наносекундах. Если два объекта созданы в один момент, они выдадут одинаковые числа.
  • Float и Double: Random.nextFloat() даёт числа от 0.0 до 1.0 (не включая 1.0). Чтобы получить число от min до max, используйте: min + Random.nextFloat() * (max - min).

Эксперимент: как выглядит «случайность» под микроскопом?

Давайте напишем мини-тест. Сгенерируем 1000 чисел от 0 до 10 и посмотрим на распределение:

import kotlin.random.Random

fun main() {
    val counts = mutableMapOf<Int, Int>()
    repeat(1000) {
        val num = Random.nextInt(0, 11)
        counts[num] = (counts[num] ?: 0) + 1
    }
    counts.toSortedMap().forEach { (key, value) ->
        println("$key: ${"*".repeat(value / 10)} ($value)")
    }
}

Вы увидите, что гистограмма более-менее равномерная. Но если запустить тест с SecureRandom — картина будет похожей, но seed будет меняться каждый раз непредсказуемо. Визуально раз

Блоги

Рекомендаций по книгам