По мотивам твита от Evgeny Mandrikov aka godin:
Quiz: without running #Java compiler, do you know how many constants can be defined in enum?— Evgeny Mandrikov (@_godin_) December 13, 2019
В нём он задаётся вопросом, какое максимальное количество значений может быть определено в перечислении (
enum
) в Java. После ряда экспериментов и применения чёрной магии ConstantDynamic (JEP 309) автор вопроса приходит к числу 8191.В серии из двух статей поищем теоретические пределы числа элементов в перечислении, попробуем к ним приблизиться на практике и попутно выясним, чем может помочь JEP 309.
Рекогносцировка
Обзорная глава, в которой мы впервые видим перечисление дизассемблированным.Для начала посмотрим, во что транслируется следующее перечисление:
public enum FizzBuzz {
Fizz,
Buzz,
FizzBuzz;
}
После компиляции и дизассемблирования:
javap -c -s -p -v FizzBuzz.class
В листинге нас встречают
— По одному
public static final
полю для каждого значения, определённого в перечислении— Приватное синтетическое поле
$VALUES
, деталь реализации метода values()
— Реализация методов
values()
и valueOf()
— Приватный конструктор
— Блок статической инициализации, где собственно и происходит всё самое интересное. Рассмотрим его подробнее.
В виде java-кода последний выглядит примерно так:
static {
Fizz = new FizzBuzz("Fizz", 0);
Buzz = new FizzBuzz("Buzz", 1);
FizzBuzz = new FizzBuzz("FizzBuzz", 2);
$VALUES = new FizzBuzz[] {
Fizz,
Buzz,
FizzBuzz
};
}
Вначале создаются экземпляры элементов перечисления. Созданные экземпляры немедленно записываются в соответствующие
public static final
поля.Затем создаётся и заполняется массив со ссылками на экземпляры всех элементы перечисления. Ссылки достаются из полей класса, которые мы инициализировали абзацем выше. Заполненный массив сохраняется в
private static final
поле $VALUES
.После этого перечисление готово к работе.
Бутылочное горлышко
Скучная глава, в которой мы ищем ограничения на количество элементов перечисления.
Начать поиски можно с главы JLS §8.9.3 «Enum Members»:
Итак, у каждого класса-перечисления есть метод
values()
, который возвращает массив со всеми объявленными в данном перечислении элементами. Из этого следует, что сферическое перечисление в вакууме не может содержать более Integer.MAX_VALUE + 1
элементов.Движемся дальше. Перечисления в Java представляются в виде наследников класса
java.lang.Enum
, а следовательно на них распространяются все ограничения, присущие классам в JVM.Посмотрим на высокоуровневое описание структуры class-файла, приведённое в JVMS §4.1 «The ClassFile Structure»:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Как мы уже знаем из JLS §8.9.3, для каждого элемента перечисления в результирующем классе создаётся одноимённое поле. Число полей в классе задаёт 16-битное беззнаковое
fields_count
, что ограничивает нас 65_535 полями в одном class-файле или 65_534 элементами перечисления. Одно поле зарезервировано под массив $VALUES
, клон которого возвращает метод values()
. Это не прописано явно в спецификации, но вряд ли получится придумать более изящное решение.Имена полей, методов, классов, константные значения и многое другое хранится в пуле констант.
Если вы совсем ничего не знаете про внутреннее устройство пула констант, рекомендую почитать древнюю статью от lany. Не смотря на то, что с момента её написания в пуле констант появилось много нового и интересного, основные принципы остаются неизменными.
Размер пула констант класса ограничен также числом в 65_535 элементов. Пул констант корректно сформированного класса никогда не бывает пуст. Как минимум, там будет имя этого класса.
К примеру, пул констант класса пустого перечисления, скомпилированный javac из OpenJDK 14-ea+29 без отладочной информации содержит 29 вхождений.
Из этого следует, что число в 65_534 элемента в одном перечислении также недостижимо. В лучшем случае можем рассчитывать на 65_505 или близкое к этому число.
Последний аккод в этом затянувшемся вступлении:
Записать значение в
static final
поле можно только в блоке статической инициализации, который на уровне class-файла представлен методом с именем <clinit>
. Байткод любого метода при этом не может занимать более 65_535 байтов. Знакомое число, не правда ли?Одна инструкция записи в статическое поле
putstatic
занимает 3 байта, что даёт нам грубую оценку в 65_535 / 3 = 21_845
. На самом деле эта оценка завышена. Значение для записи в поле инструкция берёт с вершины стека, которое туда поместила одна из предыдущих инструкций. И эта инструкция тоже занимает драгоценные байты. Но даже если не принимать это во внимание, полученное число всё равно значительно меньше 65_505.Резюмируя:
— Формат class-файла ограничивает максимальное число элементов перечисления примерно 65_505
— Механизм инициализации static final полей ограничивает нас ещё сильнее. Теоретически — до 21_845 элементов максимум, на практике это число ещё меньше
В заключительной статье цикла займёмся нездоровой оптимизацией и генерацией class-файлов.
Источник: https://habr.com/ru/post/483392/?utm_source=habrahabr&utm_medium=rss&utm_campaign=483392
Смотри также:
Функциональное программирование: в Java и C# слишком много церемоний. http://fetisovvs.blogspot.com/2017/05/java-c.html
Комментариев нет:
Отправить комментарий