Primera parte, teórica |
Segunda parte, práctica
Basado en un tweet de Evgeny Mandrikov, también conocido como
godin :
En él, se pregunta qué número máximo de valores se puede especificar en una
enum
en Java. Después de una serie de experimentos y el uso de la magia negra ConstantDynamic (
JEP 309 ), el autor de la pregunta llega al número 8191.
En una serie de dos artículos, buscamos los límites teóricos sobre el número de elementos en una enumeración, tratamos de acercarnos a ellos en la práctica y descubrimos en el camino cómo JEP 309 puede ayudar.
Reconocimiento
Un capítulo de revisión en el que primero vemos la enumeración desmontada.Primero, veamos en qué se traduce la siguiente enumeración:
public enum FizzBuzz { Fizz, Buzz, FizzBuzz; }
Después de compilar y desarmar:
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
En el listado nos encontramos
- Un campo
public static final
para cada valor definido en la enumeración - Campo sintético privado
$VALUES
, detalle de implementación del método de values()
- Implementación de los métodos
values()
y valueOf()
- Constructor privado
- El bloque de inicialización estática, donde en realidad sucede lo más interesante. Consideremos con más detalle.
En forma de código Java, este último se parece a esto:
static { Fizz = new FizzBuzz("Fizz", 0); Buzz = new FizzBuzz("Buzz", 1); FizzBuzz = new FizzBuzz("FizzBuzz", 2); $VALUES = new FizzBuzz[] { Fizz, Buzz, FizzBuzz }; }
Primero, se crean instancias de elementos de enumeración. Las instancias creadas se escriben inmediatamente en los campos
public static final
correspondientes.
Luego se crea una matriz y se completa con enlaces a instancias de todos los elementos de enumeración. Los enlaces se toman de los campos de la clase que inicializamos en el párrafo anterior. La matriz llena se almacena en el campo
private static final
$VALUES
.
Después de eso, la lista está lista para comenzar.
Cuello de botella
Un capítulo aburrido en el que buscamos restricciones en el número de elementos de enumeración.Puede comenzar su búsqueda con el capítulo
JLS §8.9.3 "Miembros de Enum":
JLS 8.9.3 Miembros de EnumLos miembros de una enumeración tipo E son todos los siguientes:
...
* Para cada enum constante c declarada en el cuerpo de la declaración de E, E tiene
un campo final estático público declarado implícitamente de tipo E que tiene el mismo
nombre como c. El campo tiene un inicializador variable que crea una instancia de E y pasa cualquier
argumentos de c para el constructor elegido para E. El campo tiene las mismas anotaciones
como c (si existe).
Estos campos se declaran implícitamente en el mismo orden que el correspondiente
constantes enum, antes de cualquier campo estático declarado explícitamente en el cuerpo del
declaración de E.
...
* Los siguientes métodos declarados implícitamente:
public static E[] values(); public static E valueOf(String name);
Por lo tanto, cada clase de enumeración tiene un método de
values()
que devuelve una matriz con todos los elementos declarados en esta enumeración. De ello se deduce que una enumeración esférica en el vacío no puede contener más de elementos
Integer.MAX_VALUE + 1
.
Siguiendo adelante. Las enumeraciones en Java se representan como descendientes de la clase
java.lang.Enum
y, por lo tanto, están sujetas a todas las restricciones inherentes a las clases en la JVM.
Veamos la descripción de alto nivel de la estructura del archivo de clase que figura en
JVMS §4.1 "La estructura de archivos de clase":
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]; }
Como ya sabemos por JLS §8.9.3, se crea un campo con el mismo nombre para cada elemento de enumeración en la clase resultante. El número de campos en la clase define un número de campos sin signo de 16 bits, lo que nos limita a 65_535 campos en un archivo de clase o 65_534 elementos de enumeración. Un campo está reservado para la matriz
$VALUES
, cuyo clon devuelve el método
values()
. Esto no se establece explícitamente en la especificación, pero es poco probable que se encuentre con una solución más elegante.
Los nombres de los campos, métodos, clases, valores constantes y mucho más se almacenan en el grupo constante.
Si no sabe nada sobre la estructura interna del grupo constante, le recomiendo leer el artículo antiguo de lany . A pesar de que desde su escritura en el grupo de constantes han aparecido muchas cosas nuevas e interesantes, los principios básicos permanecen sin cambios.
El tamaño del grupo de constantes de clase también está limitado por el número de elementos 65_535. El conjunto de constantes de una clase formada correctamente nunca está vacío. Como mínimo, habrá un nombre para esta clase.
Por ejemplo, el grupo de constantes de la clase de enumeración vacía compilada por javac desde OpenJDK 14-ea + 29 sin información de depuración contiene 29 entradas.
De ello se deduce que el número de 65_534 elementos en una enumeración también es inalcanzable. En el mejor de los casos, podemos contar con 65_505 o un número cercano a este.
El último acorde en esta introducción prolongada:
El valor se puede
<clinit>
en el campo
static final
solo en el bloque de inicialización estático, que se representa en el nivel del archivo de clase mediante un método llamado
<clinit>
. El código de bytes de cualquier método no puede ocupar más de 65_535 bytes. Un número familiar, ¿no?
Una instrucción de escritura estática
putstatic
toma 3 bytes, lo que nos da una estimación aproximada de
65_535 / 3 = 21_845
. De hecho, esta estimación está exagerada. La instrucción toma el valor para escribir en el campo desde la parte superior de la pila, que una de las instrucciones anteriores colocó allí. Y esta instrucción también toma bytes preciosos. Pero incluso si no tiene esto en cuenta, el número resultante sigue siendo significativamente menor que 65_505.
En resumen:
- El formato de archivo de clase limita el número máximo de elementos de enumeración a aproximadamente 65_505
- El mecanismo de inicialización de campo final estático nos limita aún más. Teóricamente: hasta 21_845 elementos como máximo, en la práctica, este número es aún menor
En el artículo final de la serie, nos centraremos en la optimización poco saludable y la generación de archivos de clase.