Генерация случайных строк в Java: от хаоса к производительности

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

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

Начать курс

Как сгенерировать случайную строку в Java и не сойти с ума?

Представьте: вы пишете регистрацию на сайте. Нужно создать уникальный токен для подтверждения email. Или генерировать пароль для нового пользователя. Или просто сделать читаемый ID для ссылки. Первое, что приходит в голову — взять UUID.randomUUID(), отрезать дефисы и радоваться жизни.

Стоп. А вы знаете, что простое на первый взгляд действие может превратиться в бутылочное горлышко производительности? Вопрос из Stack Overflow "How to generate a random alpha-numeric string" собрал тысячи просмотров не просто так. Дьявол, как всегда, кроется в деталях: выбор генератора случайных чисел, алгоритм, работа с памятью. В этой статье мы не просто напишем код — мы оптимизируем его до максимума.

Готовьте IntelliJ IDEA. Поехали!

1. Самый простой (но медленный) способ: UUID

Начнем с классики. UUID генерирует 128-битное значение. Если убрать дефисы, получится 32 символа в hex-формате (0-9, a-f). Это уже буквенно-цифровая строка, но с ограниченным алфавитом.

public static String generateWithUUID() {
    return UUID.randomUUID().toString().replace("-", "");
}

Плюсы: Одна строка кода. Не нужно думать.

Минусы: Нельзя контролировать длину. Только hex-символы (16 символов алфавита). Для паролей — слабовато. Для токенов — сойдет, но есть нюанс.

Кстати, UUID.randomUUID() использует SecureRandom под капотом. Это криптостойко, но медленно. Если вам нужно 10 000 строк в секунду — этот метод вас разочарует.

2. RandomStringUtils из Apache Commons Lang

Классика, которая кочует из проекта в проект. Библиотека commons-lang3 предлагает готовый метод:

import org.apache.commons.lang3.RandomStringUtils;

public static String generateWithCommons(int length) {
    return RandomStringUtils.randomAlphanumeric(length);
}

Красиво? Да. Производительно? Не очень.

Под капотом RandomStringUtils использует java.util.Random (не криптостойкий) и создает много временных объектов. Для большинства задач хватит, но если вы генерируете миллионы строк — готовьтесь к GC-паузам.

3. Кастомный метод с Random.nextInt()

Давайте напишем свой велосипед. Самый очевидный способ:

import java.security.SecureRandom;
import java.util.Random;

public class RandomStringGenerator {
    private static final String ALPHANUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    private static final int BASE = ALPHANUMERIC.length();
    private final Random random;

    public RandomStringGenerator() {
        this.random = new SecureRandom(); // для криптостойкости
    }

    public String generate(int length) {
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            sb.append(ALPHANUMERIC.charAt(random.nextInt(BASE)));
        }
        return sb.toString();
    }
}

Что здесь важно?

  • Мы используем StringBuilder с известной емкостью — не будет лишних расширений.
  • SecureRandom гарантирует, что вывод нельзя предсказать. Для токенов — мастхэв.
  • Алфавит задан строкой — 62 символа (26+26+10). Можно добавить спецсимволы.

Но есть проблема: SecureRandom может быть медленным на Linux (ожидание энтропии). Если скорость важнее безопасности — используйте ThreadLocalRandom.current().

4. Оптимизация с char[] и ThreadLocalRandom

Хотите выжать максимум? Отказываемся от StringBuilder и работаем напрямую с массивом символов:

import java.util.concurrent.ThreadLocalRandom;

public class FastRandomString {
    private static final char[] ALPHANUMERIC = 
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
    private static final int BASE = ALPHANUMERIC.length;

    public static String generate(int length) {
        char[] result = new char[length];
        ThreadLocalRandom random = ThreadLocalRandom.current();
        for (int i = 0; i < length; i++) {
            result[i] = ALPHANUMERIC[random.nextInt(BASE)];
        }
        return new String(result);
    }
}

Почему это быстрее?

  • ThreadLocalRandom — без блокировок, отлично для многопоточки.
  • char[] — нет оверхеда StringBuilder. Сразу пишем в массив.
  • new String(result) — создает строку без копирования массива (в современных JVM).

Этот метод на моем ноутбуке генерирует 10 млн строк длиной 10 символов за ~0.8 секунды. RandomStringUtils делает это за 3.5 секунды. Чувствуете разницу?

5. Stream API — красиво, но медленно?

Java 8 принесла Stream API. Многие пишут так:

import java.util.stream.Collectors;
import java.util.stream.IntStream;

public static String generateWithStreams(int length) {
    return IntStream.range(0, length)
        .map(i -> ThreadLocalRandom.current().nextInt(62))
        .mapToObj(i -> String.valueOf(ALPHANUMERIC[i]))
        .collect(Collectors.joining());
}

Код компактный, но для производительности — убийца. Каждый String.valueOf создает новую строку, а Collectors.joining() внутри использует StringBuilder, но с дополнительными накладными расходами.

Мой тест: Stream API в 5 раз медленнее метода с char[]. Вывод: используйте Stream только для небольших строк (до 10 символов) или когда производительность не критична.

6. Бенчмаркинг: кто кого?

Давайте посмотрим на цифры. Тест: 1 миллион строк длиной 10 символов. Используем JMH (Java Microbenchmark Harness).

Блоги

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

Метод Время (мс) Пропускная способность (строк/сек)
UUID (hex) 1 200 ~830 000
RandomStringUtils 350 ~2 850 000
SecureRandom + StringBuilder 280