Número máximo de valores em enumeração, parte I

Primeira parte, teórica | Parte Dois, Prática


Baseado em um tweet de Evgeny Mandrikov aka godin :


Nele, ele se pergunta que número máximo de valores pode ser especificado em uma enum em Java. Após uma série de experimentos e o uso da magia negra ConstantDynamic ( JEP 309 ), o autor da pergunta chega ao número 8191.

Em uma série de dois artigos, procuramos os limites teóricos do número de elementos em uma enumeração, tentamos nos aproximar deles na prática e descobrimos ao longo do caminho como o JEP 309 pode ajudar.

Reconhecimento


Um capítulo de revisão no qual vemos pela primeira vez a enumeração desmontada.

Primeiro, vamos ver o que a seguinte enumeração se traduz:

 public enum FizzBuzz { Fizz, Buzz, FizzBuzz; } 

Depois de compilar e desmontar:

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 // FizzBuzz.$VALUES:[LFizzBuzz; #2 = Class #4 // FizzBuzz #3 = NameAndType #5:#6 // $VALUES:[LFizzBuzz; #4 = Utf8 FizzBuzz #5 = Utf8 $VALUES #6 = Utf8 [LFizzBuzz; #7 = Methodref #8.#9 // "[LFizzBuzz;".clone:()Ljava/lang/Object; #8 = Class #6 // "[LFizzBuzz;" #9 = NameAndType #10:#11 // clone:()Ljava/lang/Object; #10 = Utf8 clone #11 = Utf8 ()Ljava/lang/Object; #12 = Methodref #13.#14 // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; #13 = Class #15 // java/lang/Enum #14 = NameAndType #16:#17 // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; #15 = Utf8 java/lang/Enum #16 = Utf8 valueOf #17 = Utf8 (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; #18 = Methodref #13.#19 // java/lang/Enum."<init>":(Ljava/lang/String;I)V #19 = NameAndType #20:#21 // "<init>":(Ljava/lang/String;I)V #20 = Utf8 <init> #21 = Utf8 (Ljava/lang/String;I)V #22 = String #23 // Fizz #23 = Utf8 Fizz #24 = Methodref #2.#19 // FizzBuzz."<init>":(Ljava/lang/String;I)V #25 = Fieldref #2.#26 // FizzBuzz.Fizz:LFizzBuzz; #26 = NameAndType #23:#27 // Fizz:LFizzBuzz; #27 = Utf8 LFizzBuzz; #28 = String #29 // Buzz #29 = Utf8 Buzz #30 = Fieldref #2.#31 // FizzBuzz.Buzz:LFizzBuzz; #31 = NameAndType #29:#27 // Buzz:LFizzBuzz; #32 = String #4 // FizzBuzz #33 = Fieldref #2.#34 // FizzBuzz.FizzBuzz:LFizzBuzz; #34 = NameAndType #4:#27 // FizzBuzz:LFizzBuzz; #35 = Utf8 values #36 = Utf8 ()[LFizzBuzz; #37 = Utf8 Code #38 = Utf8 LineNumberTable #39 = Utf8 (Ljava/lang/String;)LFizzBuzz; #40 = Utf8 LocalVariableTable #41 = Utf8 name #42 = Utf8 Ljava/lang/String; #43 = Utf8 this #44 = Utf8 Signature #45 = Utf8 ()V #46 = Utf8 <clinit> #47 = Utf8 Ljava/lang/Enum<LFizzBuzz;>; #48 = Utf8 SourceFile #49 = Utf8 FizzBuzz.java { public static final FizzBuzz Fizz; descriptor: LFizzBuzz; flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static final FizzBuzz Buzz; descriptor: LFizzBuzz; flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static final FizzBuzz FizzBuzz; descriptor: LFizzBuzz; flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM private static final FizzBuzz[] $VALUES; descriptor: [LFizzBuzz; flags: (0x101a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC public static FizzBuzz[] values(); descriptor: ()[LFizzBuzz; flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=0, args_size=0 0: getstatic #1 // Field $VALUES:[LFizzBuzz; 3: invokevirtual #7 // Method "[LFizzBuzz;".clone:()Ljava/lang/Object; 6: checkcast #8 // class "[LFizzBuzz;" 9: areturn LineNumberTable: line 1: 0 public static FizzBuzz valueOf(java.lang.String); descriptor: (Ljava/lang/String;)LFizzBuzz; flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: ldc #2 // class FizzBuzz 2: aload_0 3: invokestatic #12 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; 6: checkcast #2 // class FizzBuzz 9: areturn LineNumberTable: line 1: 0 LocalVariableTable: Start Length Slot Name Signature 0 10 0 name Ljava/lang/String; private FizzBuzz(); descriptor: (Ljava/lang/String;I)V flags: (0x0002) ACC_PRIVATE Code: stack=3, locals=3, args_size=3 0: aload_0 1: aload_1 2: iload_2 3: invokespecial #18 // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V 6: return LineNumberTable: line 1: 0 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this LFizzBuzz; Signature: #45 // ()V static {}; descriptor: ()V flags: (0x0008) ACC_STATIC Code: stack=4, locals=0, args_size=0 0: new #2 // class FizzBuzz 3: dup 4: ldc #22 // String Fizz 6: iconst_0 7: invokespecial #24 // Method "<init>":(Ljava/lang/String;I)V 10: putstatic #25 // Field Fizz:LFizzBuzz; 13: new #2 // class FizzBuzz 16: dup 17: ldc #28 // String Buzz 19: iconst_1 20: invokespecial #24 // Method "<init>":(Ljava/lang/String;I)V 23: putstatic #30 // Field Buzz:LFizzBuzz; 26: new #2 // class FizzBuzz 29: dup 30: ldc #32 // String FizzBuzz 32: iconst_2 33: invokespecial #24 // Method "<init>":(Ljava/lang/String;I)V 36: putstatic #33 // Field FizzBuzz:LFizzBuzz; 39: iconst_3 40: anewarray #2 // class FizzBuzz 43: dup 44: iconst_0 45: getstatic #25 // Field Fizz:LFizzBuzz; 48: aastore 49: dup 50: iconst_1 51: getstatic #30 // Field Buzz:LFizzBuzz; 54: aastore 55: dup 56: iconst_2 57: getstatic #33 // Field FizzBuzz:LFizzBuzz; 60: aastore 61: putstatic #1 // Field $VALUES:[LFizzBuzz; 64: return LineNumberTable: line 3: 0 line 5: 13 line 7: 26 line 1: 39 } Signature: #47 // Ljava/lang/Enum<LFizzBuzz;>; SourceFile: "FizzBuzz.java" 


Na listagem somos atendidos

  • Um campo public static final para cada valor definido na enumeração
  • Campo sintético privado $VALUES , detalhes de implementação do método values()
  • Implementação dos métodos values() e valueOf()
  • Construtor privado
  • O bloco de inicialização estática, onde realmente acontece a coisa mais interessante. Vamos considerar com mais detalhes.

Na forma de código java, o último se parece com isso:

  static { Fizz = new FizzBuzz("Fizz", 0); Buzz = new FizzBuzz("Buzz", 1); FizzBuzz = new FizzBuzz("FizzBuzz", 2); $VALUES = new FizzBuzz[] { Fizz, Buzz, FizzBuzz }; } 

Primeiro, são criadas instâncias de elementos de enumeração. Instâncias criadas são gravadas imediatamente nos campos public static final correspondentes.

Em seguida, uma matriz é criada e preenchida com links para instâncias de todos os elementos de enumeração. Os links são obtidos dos campos da classe que inicializamos no parágrafo acima. A matriz preenchida é armazenada no campo private static final $VALUES .

Depois disso, a listagem está pronta para ser usada.

Gargalo


Um capítulo chato no qual procuramos restrições no número de elementos de enumeração.

Você pode iniciar sua pesquisa no capítulo 8.9.3 da JLS, “Enum Members”:

Membros do Enum do JLS 8.9.3
Os membros de uma enumeração tipo E são todos os seguintes:
...
* Para cada constante enum c declarada no corpo da declaração de E, E tem
um campo final estático público declarado implicitamente do tipo E que possui o mesmo
nome como c. O campo possui um inicializador variável que instancia E e passa qualquer
argumentos de c para o construtor escolhido para E. O campo tem as mesmas anotações
como c (se houver).

Esses campos são declarados implicitamente na mesma ordem que o correspondente
constantes enum, antes de qualquer campo estático declarado explicitamente no corpo do
declaração de E.
...
* Os seguintes métodos declarados implicitamente:
 /** * Returns an array containing the constants of this enum * type, in the order they're declared. This method may be * used to iterate over the constants as follows: * * for(E c : E.values()) * System.out.println(c); * * @return an array containing the constants of this enum * type, in the order they're declared */ public static E[] values(); /** * Returns the enum constant of this type with the specified * name. * The string must match exactly an identifier used to declare * an enum constant in this type. (Extraneous whitespace * characters are not permitted.) * * @return the enum constant with the specified name * @throws IllegalArgumentException if this enum type has no * constant with the specified name */ public static E valueOf(String name); 



Portanto, cada classe de enumeração possui um método values() que retorna uma matriz com todos os elementos declarados nessa enumeração. Daqui resulta que uma enumeração esférica no vácuo não pode conter mais do que elementos Integer.MAX_VALUE + 1 .

Seguindo em frente. As enumerações em Java são representadas como descendentes da classe java.lang.Enum e, portanto, estão sujeitas a todas as restrições inerentes às classes na JVM.

Vejamos a descrição de alto nível da estrutura do arquivo de classe fornecida na JVMS §4.1 “A Estrutura do 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]; } 

Como já sabemos no JLS §8.9.3, um campo com o mesmo nome é criado para cada elemento de enumeração na classe resultante. O número de campos na classe define um fields_count não assinado de 16 bits, o que nos limita a 65_535 campos em um arquivo de classe ou 65_534 elementos de enumeração. Um campo é reservado para a matriz $VALUES , cujo clone retorna o método values() . Isso não é explicitamente declarado na especificação, mas é improvável que surja uma solução mais elegante.

Os nomes dos campos, métodos, classes, valores constantes e muito mais são armazenados no pool constante.
Se você não sabe nada sobre a estrutura interna do pool constante, recomendo a leitura do artigo antigo da lany . Apesar do fato de que, desde a sua escrita no conjunto de constantes, muitas coisas novas e interessantes apareceram, os princípios básicos permanecem inalterados.
O tamanho do pool de constantes de classe também é limitado pelo número de 65_535 elementos. O conjunto de constantes de uma classe formada corretamente nunca está vazio. No mínimo, haverá um nome para esta classe.

Por exemplo, o conjunto de constantes da classe de enumeração vazia compilada pelo javac do OpenJDK 14-ea + 29 sem informações de depuração contém 29 ocorrências.

Daqui resulta que o número de 65_534 elementos em uma enumeração também é inatingível. Na melhor das hipóteses, podemos contar com 65_505 ou um número próximo a isso.

O último acorde nesta introdução prolongada:

O valor pode ser <clinit> no campo static final apenas no bloco de inicialização estático, representado no nível do arquivo de classe por um método chamado <clinit> . O bytecode de qualquer método não pode ocupar mais de 65_535 bytes. Um número familiar, não é?

Uma instrução de gravação estática putstatic leva 3 bytes, o que nos dá uma estimativa aproximada de 65_535 / 3 = 21_845 . De fato, essa estimativa é exagerada. A instrução assume o valor para gravar no campo a partir do topo da pilha, que uma das instruções anteriores colocou lá. E esta instrução também ocupa bytes preciosos. Mas mesmo se você não levar isso em consideração, o número resultante ainda será significativamente menor que 65_505.

Em resumo:

  • O formato do arquivo de classe limita o número máximo de elementos de enumeração para aproximadamente 65_505
  • O mecanismo estático de inicialização do campo final nos limita ainda mais. Teoricamente - até 21_845 elementos no máximo, na prática esse número é ainda menos

No artigo final da série, focaremos na otimização não saudável e na geração de arquivos de classe.

Source: https://habr.com/ru/post/pt483392/


All Articles