Nombre maximal de valeurs dans l'énumération, partie I

Première partie, théorique | Deuxième partie, pratique


Basé sur un tweet d'Evgeny Mandrikov alias godin :


Dans ce document, il se demande quel nombre maximal de valeurs peut être spécifié dans une enum en Java. Après une série d'expériences et l'utilisation de la magie noire ConstantDynamic ( JEP 309 ), l'auteur de la question arrive au nombre 8191.

Dans une série de deux articles, nous recherchons les limites théoriques du nombre d'éléments dans une énumération, essayons de nous en rapprocher dans la pratique et découvrons en cours de route comment le JEP 309 peut aider.

Reconnaissance


Un chapitre de revue dans lequel nous voyons d'abord l'énumération démontée.

Voyons d'abord en quoi l'énumération suivante se traduit:

 public enum FizzBuzz { Fizz, Buzz, FizzBuzz; } 

Après avoir compilé et démonté:

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" 


En listant nous sommes rencontrés

  • Un public static final pour chaque valeur définie dans l'énumération
  • Champ synthétique privé $VALUES , détail d'implémentation de la méthode values()
  • Implémentation des méthodes values() et valueOf()
  • Constructeur privé
  • Le bloc d'initialisation statique, où se produit en fait la chose la plus intéressante. Examinons-le plus en détail.

Sous forme de code java, ce dernier ressemble à ceci:

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

Tout d'abord, des instances d'éléments d'énumération sont créées. Les instances créées sont immédiatement écrites dans les champs public static final correspondants.

Un tableau est ensuite créé et rempli de liens vers des instances de tous les éléments d'énumération. Les liens proviennent des champs de la classe que nous avons initialisés dans le paragraphe ci-dessus. Le tableau rempli est stocké dans le private static final $VALUES .

Après cela, la liste est prête à être lancée.

Goulot d'étranglement


Un chapitre ennuyeux dans lequel nous recherchons des restrictions sur le nombre d'éléments d'énumération.

Vous pouvez commencer votre recherche avec le chapitre JLS §8.9.3 «Enum Members»:

JLS 8.9.3 Enum Members
Les membres d'une énumération de type E sont tous les suivants:
...
* Pour chaque constante enum c déclarée dans le corps de la déclaration de E, E a
un champ final public statique implicitement déclaré de type E qui a le même
nom comme c. Le champ a un initialiseur variable qui instancie E et passe tout
arguments de c au constructeur choisi pour E. Le champ a les mêmes annotations
comme c (le cas échéant).

Ces champs sont implicitement déclarés dans le même ordre que les champs correspondants
énumérations constantes, avant tout champ statique explicitement déclaré dans le corps du
déclaration de E.
...
* Les méthodes déclarées implicitement suivantes:
 /** * 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); 



Ainsi, chaque classe d'énumération a une méthode values() qui retourne un tableau avec tous les éléments déclarés dans cette énumération. Il s'ensuit qu'une énumération sphérique dans le vide ne peut pas contenir plus de Integer.MAX_VALUE + 1 éléments.

Continuons. Les énumérations en Java sont représentées comme des descendants de la classe java.lang.Enum et sont donc soumises à toutes les restrictions inhérentes aux classes de la JVM.

Regardons la description de haut niveau de la structure du fichier de classe donnée dans JVMS §4.1 «La structure 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]; } 

Comme nous le savons déjà par JLS §8.9.3, un champ du même nom est créé pour chaque élément d'énumération dans la classe résultante. Le nombre de champs dans la classe définit un nombre de champs non signé de 16 bits, ce qui nous limite à 65_535 champs dans un fichier de classe ou à des éléments d'énumération 65_534. Un champ est réservé au tableau $VALUES , dont un clone renvoie la méthode values() . Ce n'est pas explicitement indiqué dans la spécification, mais il est peu probable de trouver une solution plus élégante.

Les noms des champs, méthodes, classes, valeurs constantes et bien plus sont stockés dans le pool constant.
Si vous ne savez rien de la structure interne de la piscine constante, je vous recommande de lire l' ancien article de lany . Malgré le fait que depuis son écriture dans le pool de constantes beaucoup de choses nouvelles et intéressantes sont apparues, les principes de base restent inchangés.
La taille du pool de constantes de classe est également limitée par le nombre d'éléments 65_535. Le pool de constantes d'une classe correctement formée n'est jamais vide. Au minimum, il y aura un nom pour cette classe.

Par exemple, le pool de constantes de la classe d'une énumération vide compilé par javac à partir d'OpenJDK 14-ea + 29 sans informations de débogage contient 29 entrées.

Il s'ensuit que le nombre de 65_534 éléments dans une énumération est également inaccessible. Dans le meilleur des cas, nous pouvons compter sur 65_505 ou un nombre proche de celui-ci.

Le dernier accord de cette introduction prolongée:

La valeur peut être <clinit> dans le static final uniquement dans le bloc d'initialisation statique, qui est représenté au niveau du fichier de classe par une méthode appelée <clinit> . Le bytecode d'une méthode ne peut pas occuper plus de 65_535 octets. Un chiffre familier, n'est-ce pas?

Une putstatic d'écriture statique putstatic prend 3 octets, ce qui nous donne une estimation approximative de 65_535 / 3 = 21_845 . En fait, cette estimation est surestimée. L'instruction prend la valeur à écrire dans le champ à partir du haut de la pile, que l'une des instructions précédentes y a placée. Et cette instruction prend également de précieux octets. Mais même si vous n'en tenez pas compte, le nombre résultant est toujours nettement inférieur à 65_505.

En résumé:

  • Le format de fichier de classe limite le nombre maximal d'éléments d'énumération à environ 65_505
  • Le mécanisme d'initialisation du champ final statique nous limite encore plus. Théoriquement - jusqu'à 21_845 éléments maximum, en pratique ce nombre est encore moins

Dans le dernier article de la série, nous nous concentrerons sur l'optimisation malsaine et la génération de fichiers de classe.

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


All Articles