第一部分,理论 |
第二部分,实用
根据Evgeny Mandrikov aka
Godin的推文:
在其中,他想知道在Java
enum
中可以指定最大数量的值。 经过一系列实验和使用黑魔法ConstantDynamic(
JEP 309 ),问题的作者得出了编号8191。
在由两篇文章组成的系列文章中,我们寻找枚举中元素数量的理论极限,尝试在实践中更接近它们,并找出JEP 309可以如何提供帮助。
侦察
回顾章节,我们首先看到枚举被分解。首先,让我们看一下以下枚举的含义:
public enum FizzBuzz { Fizz, Buzz, FizzBuzz; }
编译和拆卸后:
javap -c -s -p -v FizzBuzz.class Classfile /dev/null/FizzBuzz.class Last modified 32 . 2019 .; size 903 bytes MD5 checksum add0af79de3e9a70a7bbf7d57dd0cfe7 Compiled from "FizzBuzz.java" public final class FizzBuzz extends java.lang.Enum<FizzBuzz> minor version: 0 major version: 58 flags: (0x4031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM this_class: #2 // FizzBuzz super_class: #13 // java/lang/Enum interfaces: 0, fields: 4, methods: 4, attributes: 2 Constant pool: #1 = Fieldref #2.#3
在上市我们遇到了
- 枚举中定义的每个值的一个
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节 “枚举成员”开始搜索:
JLS 8.9.3枚举成员枚举类型E的成员都是以下所有:
...
*对于E声明主体中声明的每个枚举常量c,E具有
具有相同类型的隐式声明的E型公共静态最终字段
名称为c。 该字段具有变量初始化器,该初始化器实例化E并传递任何
c为为E选择的构造函数的参数。该字段具有相同的批注
作为c(如果有)。
这些字段以与相应字段相同的顺序隐式声明
枚举常量,在主体中显式声明的任何静态字段之前
E的声明
...
*以下隐式声明的方法:
public static E[] values(); public static E valueOf(String name);
因此,每个枚举类都有一个
values()
方法,该方法返回一个包含此枚举中声明的所有元素的数组。 因此,真空中的球形枚举不能包含超过
Integer.MAX_VALUE + 1
元素。
继续前进。 Java中的枚举表示为
java.lang.Enum
类的后代,因此它们受JVM中类固有的所有限制。
让我们看一下JVMS§4.1“ ClassFile结构”中给出的类文件结构的高级描述:
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_534枚举元素中的65_535个字段。
$VALUES
数组保留一个字段,该字段的一个克隆返回
values()
方法。 规范中没有明确说明,但是不太可能提出更优雅的解决方案。
字段,方法,类,常量值等的名称存储在常量池中。
如果您对常量池的内部结构一无所知,建议您阅读lany的古老文章 。 尽管自从将其写入常量池以来,已经出现了许多新颖有趣的东西,但基本原理仍然保持不变。
类常量池的大小也受65_535个元素的数量限制。 正确形成的类的常量池永远不会为空。 至少会有一个此类的名称。
例如,由javac从OpenJDK 14-ea + 29编译的空枚举类的常量池中没有调试信息,包含29次出现。
因此,一个枚举中65_534个元素的数量也无法达到。 在最佳情况下,我们可以指望65_505或接近这个数字。
这个冗长的介绍中的最后一个和弦是:
只能在静态初始化块中将该值
<clinit>
到
static final
字段,该块在类文件级别由称为
<clinit>
的方法表示。 任何方法的字节码不能超过65_535个字节。 一个熟悉的数字,不是吗?
一条静态静态写入指令占用3个字节,这给了我们大约
65_535 / 3 = 21_845
估计值。 实际上,这个估计数被夸大了。 该指令采用该值从堆栈的顶部写入该字段,前面的指令之一放在该堆栈的顶部。 并且该指令还占用了宝贵的字节。 但是,即使您不考虑这一点,所得到的数字仍然明显小于65_505。
总结:
- 类文件格式将枚举元素的最大数量限制为大约65_505
- 静态最终字段初始化机制对我们的限制更大。 理论上-最多21_845个元素,实际上这个数字甚至更少
在本系列的最后一篇文章中,我们将重点讨论不健康的优化和类文件的生成。