Как заставить 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 будет меняться каждый раз непредсказуемо. Визуально раз