Enum w JVM – szczegóły implementacyjne

Kolejnym słowem kluczowym, które chciałbym omówić jest enum. Ta konstrukcja została wprowadzona w Javie 1.5. Może się wydawać, że intuicyjnie wiemy, jak technicznie enum jest zaimplementowany, jednak warto zweryfikować domysły. Być może to słowo kluczowe niesie ze sobą jakieś dodatkowe „magiczne” właściwości, których zwykła klasa nie posiada…

Pierwsze spojrzenie na bytecode

Na początek stwórzmy i skompilujmy prostą klasę:

public enum Enum {
    VAL_1(1),
    VAL_2(2);

    private final int abc;

    Enum(int abc) {
        this.abc = abc;
    }
}

Następnie dekompilujmy ją z użyciem javap -v -p. Dekompilator wyświetla dosyć dużo linii, więc skupię się na tych ciekawszych rzeczach i krótko skomentuję. Cały listing na samym spodzie postu.

public final class dev.jgardo.jvm.miscellaneous.enums.Enum extends java.lang.Enum<dev.jgardo.jvm.miscellaneous.enums.Enum>

Jak widzimy, enum jest w czasie kompilacji do bytecodu zamieniany na „zwykłą” klasę dziedziczącą z java.lang.Enum. Jednak różni się od „zwykłej” klasy flagami:

  flags: (0x4031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM

Obecność flagi ACC_ENUM może potencjalnie odpowiadać za jakieś zachowania, więc wrócimy do niej później.

  public static final dev.jgardo.jvm.miscellaneous.enums.Enum VAL_1;
    descriptor: Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final dev.jgardo.jvm.miscellaneous.enums.Enum VAL_2;
    descriptor: Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

Widzimy również dwa pola statyczne finalne z dodatkową flagą ACC_ENUM.

  private final int abc;
  private static final dev.jgardo.jvm.miscellaneous.enums.Enum[] $VALUES;
  public static dev.jgardo.jvm.miscellaneous.enums.Enum[] values();
  public static dev.jgardo.jvm.miscellaneous.enums.Enum valueOf(java.lang.String);
  private dev.jgardo.jvm.miscellaneous.enums.Enum(int);

W kolejnych liniach widzimy pole, które zadeklarowaliśmy w enumie, a następnie wygenerowane pole statyczne finalne z wszystkimi wartościami enuma o nazwie $VALUES, a po nim kolejne dwie wygenerowane statyczne metody. Pierwsza zwraca wszystkie możliwe wartości, a druga zwraca wartość enuma dla podanego Stringa.
Następnie widzimy zadeklarowany wcześniej konstruktor.

Na samym końcu listingu z javap znajduje się wygenerowana inicjalizacja wartości enumów (pól statycznych finalnych) oraz wypełnienie wartościami wspomnianej wcześniej tablicy $VALUES.

Czyli to zwykła klasa?

Można by się pokusić o stwierdzenie, że tak właściwie to słowo kluczowe enum służy do ograniczenia boilerplate’u poprzez wygenerowanie zwykłej klasy Javowej. Być może równie dobrze taki boilerplate możnaby ograniczyć jakąś adnotacją Lombokową….
Czy więc zatem można by taki enum stworzyć „ręcznie”? Warto spróbować zamienić klasę Enum

public enum Enum {
    VAL_1(1),
    VAL_2(2);

    private final int abc;
    Enum(int abc) {
        this.abc = abc;
    }
}

na odpowiadającą jej implementację wygenerowanej klasy Enuma czyli:

public class Enum extends java.lang.Enum<Enum> {
    public static final Enum VAL_1 = new Enum("VAL_1", 1, 1);
    public static final Enum VAL_2 = new Enum("VAL_2", 2, 2);

    private final int abc;
    private static final Enum[] $VALUES = new Enum[] {VAL_1, VAL_2 };
    public static Enum[] values() {
        return $VALUES;
    }

    public static Enum valueOf(String name) {
        return valueOf(Enum.class, name);
    }
    Enum(String name, int ordinal, int abc) {
        super(name, ordinal);
        this.abc = abc;
    }
}

Okazuje się, że enum jest uprzywilejowany na kilka sposobów.

1. switch pozwala na używanie enumów w case. Polega to na wywołaniu metody ordinal() enuma, co jest równe liczbie porządkowej wartości danego enuma. Dzięki temu case może dotyczyć już zwykłych intów co jest standardowym mechanizmem (zamiana wartości Enuma na wartości ordinal(), również jest automatyczna i nie widać tego w kodzie, choć w bytecodzie jest to widoczne).
Jeśli chcielibyśmy stworzyć własnoręcznie klasę, wywołanie ordinal() musialo by być jawne, co zmniejsza czytelność kodu.

2. Tworzenie obiektem z użyciem refleksji jest dla enumów zablokowane. Szybki test:

    public static void main(String[] args) throws Exception {
        var constructor = Enum.class.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        var generated = constructor.newInstance("VAL_G", 2, 2);
        System.out.println(generated);
    }

powoduje równie szybki błąd:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:484)
	at dev.jgardo.jvm.miscellaneous.enums.EnumExperiment.main(EnumExperiment.java:18)

Jeśli spojrzymy w implementację (Constructor.newInstance(Constructor.java:484)), to za to rzucenie wyjątku jest uwarunkowane obecnością wspomnianej wcześniej flagi ACC_ENUM dla danej klasy.

3. Instancje enumów można wykorzystywać w adnotacjach, instancje zwykłych klas – nie. Generalnie to jest duża przewaga, a osiągana jest ona znów dzięki fladze ACC_ENUM dla klasy.

4. Enumy są dobrze przystosowane do serializacji obiektów, które je posiadają – po deserializacji otrzymywany jest istniejący enum, a nie jakiś kolejny nowo stworzony enum (a tak by było przy w przypadku zwykłej klasy).

5. W zasadzie na końcu najważniejsze – tego sie normalnie nie da skompilować 😛 Kompilator javac uniemożliwia „ręczne” stworzenie klasy dziedziczącej po java.lang.Enum
Oświadcza to dosadnie komunikatem przy kompilacji:
Enum.java:3: error: classes cannot directly extend java.lang.Enum

Podsumowanie

Można by w skrócie powiedzieć, że enum niby jest taką zwykła klasą, ale jednak nie 😉 Bez wsparcia ze strony JVMa i kompilatora nie można by go używać w tak elastyczny sposób (w adnotacjach, switchu, serializacji). Z drugiej strony można też powiedzieć, że całość implementacji jest dosyć intuicyjna i przewidywalna i że nie ma tam jakiejś specjalnej „magii”.

Z perspektywy czasu można śmiało powiedzieć, że dodanie osobnego słowa kluczowego było krokiem w dobrą stronę.

I na koniec obiecany cały listing javap -v -p:

Classfile /home/gardziol/repository/jvm-miscellaneous/target/classes/dev/jgardo/jvm/miscellaneous/enums/Enum.class
  Last modified 6 paź 2019; size 1137 bytes
  MD5 checksum 18c950a8da67456a2509b83e2dfe7d36
  Compiled from "Enum.java"
public final class dev.jgardo.jvm.miscellaneous.enums.Enum extends java.lang.Enum<dev.jgardo.jvm.miscellaneous.enums.Enum>
  minor version: 0
  major version: 55
  flags: (0x4031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
  this_class: #4                          // dev/jgardo/jvm/miscellaneous/enums/Enum
  super_class: #13                        // java/lang/Enum
  interfaces: 0, fields: 4, methods: 4, attributes: 2
Constant pool:
   #1 = Fieldref           #4.#40         // dev/jgardo/jvm/miscellaneous/enums/Enum.$VALUES:[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
   #2 = Methodref          #41.#42        // "[Ldev/jgardo/jvm/miscellaneous/enums/Enum;".clone:()Ljava/lang/Object;
   #3 = Class              #20            // "[Ldev/jgardo/jvm/miscellaneous/enums/Enum;"
   #4 = Class              #43            // dev/jgardo/jvm/miscellaneous/enums/Enum
   #5 = Methodref          #13.#44        // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
   #6 = Methodref          #13.#45        // java/lang/Enum."<init>":(Ljava/lang/String;I)V
   #7 = Fieldref           #4.#46         // dev/jgardo/jvm/miscellaneous/enums/Enum.abc:I
   #8 = String             #14            // VAL_1
   #9 = Methodref          #4.#47         // dev/jgardo/jvm/miscellaneous/enums/Enum."<init>":(Ljava/lang/String;II)V
  #10 = Fieldref           #4.#48         // dev/jgardo/jvm/miscellaneous/enums/Enum.VAL_1:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #11 = String             #16            // VAL_2
  #12 = Fieldref           #4.#49         // dev/jgardo/jvm/miscellaneous/enums/Enum.VAL_2:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #13 = Class              #50            // java/lang/Enum
  #14 = Utf8               VAL_1
  #15 = Utf8               Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #16 = Utf8               VAL_2
  #17 = Utf8               abc
  #18 = Utf8               I
  #19 = Utf8               $VALUES
  #20 = Utf8               [Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #21 = Utf8               values
  #22 = Utf8               ()[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #23 = Utf8               Code
  #24 = Utf8               LineNumberTable
  #25 = Utf8               valueOf
  #26 = Utf8               (Ljava/lang/String;)Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #27 = Utf8               LocalVariableTable
  #28 = Utf8               name
  #29 = Utf8               Ljava/lang/String;
  #30 = Utf8               <init>
  #31 = Utf8               (Ljava/lang/String;II)V
  #32 = Utf8               this
  #33 = Utf8               Signature
  #34 = Utf8               (I)V
  #35 = Utf8               <clinit>
  #36 = Utf8               ()V
  #37 = Utf8               Ljava/lang/Enum<Ldev/jgardo/jvm/miscellaneous/enums/Enum;>;
  #38 = Utf8               SourceFile
  #39 = Utf8               Enum.java
  #40 = NameAndType        #19:#20        // $VALUES:[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #41 = Class              #20            // "[Ldev/jgardo/jvm/miscellaneous/enums/Enum;"
  #42 = NameAndType        #51:#52        // clone:()Ljava/lang/Object;
  #43 = Utf8               dev/jgardo/jvm/miscellaneous/enums/Enum
  #44 = NameAndType        #25:#53        // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  #45 = NameAndType        #30:#54        // "<init>":(Ljava/lang/String;I)V
  #46 = NameAndType        #17:#18        // abc:I
  #47 = NameAndType        #30:#31        // "<init>":(Ljava/lang/String;II)V
  #48 = NameAndType        #14:#15        // VAL_1:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #49 = NameAndType        #16:#15        // VAL_2:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #50 = Utf8               java/lang/Enum
  #51 = Utf8               clone
  #52 = Utf8               ()Ljava/lang/Object;
  #53 = Utf8               (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  #54 = Utf8               (Ljava/lang/String;I)V
{
  public static final dev.jgardo.jvm.miscellaneous.enums.Enum VAL_1;
    descriptor: Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final dev.jgardo.jvm.miscellaneous.enums.Enum VAL_2;
    descriptor: Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  private final int abc;
    descriptor: I
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

  private static final dev.jgardo.jvm.miscellaneous.enums.Enum[] $VALUES;
    descriptor: [Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x101a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC

  public static dev.jgardo.jvm.miscellaneous.enums.Enum[] values();
    descriptor: ()[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field $VALUES:[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
         3: invokevirtual #2                  // Method "[Ldev/jgardo/jvm/miscellaneous/enums/Enum;".clone:()Ljava/lang/Object;
         6: checkcast     #3                  // class "[Ldev/jgardo/jvm/miscellaneous/enums/Enum;"
         9: areturn
      LineNumberTable:
        line 3: 0

  public static dev.jgardo.jvm.miscellaneous.enums.Enum valueOf(java.lang.String);
    descriptor: (Ljava/lang/String;)Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #4                  // class dev/jgardo/jvm/miscellaneous/enums/Enum
         2: aload_0
         3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #4                  // class dev/jgardo/jvm/miscellaneous/enums/Enum
         9: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  name   Ljava/lang/String;

  private dev.jgardo.jvm.miscellaneous.enums.Enum(int);
    descriptor: (Ljava/lang/String;II)V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=3, locals=4, args_size=4
         0: aload_0
         1: aload_1
         2: iload_2
         3: invokespecial #6                  // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
         6: aload_0
         7: iload_3
         8: putfield      #7                  // Field abc:I
        11: return
      LineNumberTable:
        line 19: 0
        line 20: 6
        line 21: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Ldev/jgardo/jvm/miscellaneous/enums/Enum;
            0      12     3   abc   I
    Signature: #34                          // (I)V

  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=5, locals=0, args_size=0
         0: new           #4                  // class dev/jgardo/jvm/miscellaneous/enums/Enum
         3: dup
         4: ldc           #8                  // String VAL_1
         6: iconst_0
         7: iconst_1
         8: invokespecial #9                  // Method "<init>":(Ljava/lang/String;II)V
        11: putstatic     #10                 // Field VAL_1:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
        14: new           #4                  // class dev/jgardo/jvm/miscellaneous/enums/Enum
        17: dup
        18: ldc           #11                 // String VAL_2
        20: iconst_1
        21: iconst_2
        22: invokespecial #9                  // Method "<init>":(Ljava/lang/String;II)V
        25: putstatic     #12                 // Field VAL_2:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
        28: iconst_2
        29: anewarray     #4                  // class dev/jgardo/jvm/miscellaneous/enums/Enum
        32: dup
        33: iconst_0
        34: getstatic     #10                 // Field VAL_1:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
        37: aastore
        38: dup
        39: iconst_1
        40: getstatic     #12                 // Field VAL_2:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
        43: aastore
        44: putstatic     #1                  // Field $VALUES:[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
        47: return
      LineNumberTable:
        line 4: 0
        line 10: 14
        line 3: 28
}
Signature: #37                          // Ljava/lang/Enum<Ldev/jgardo/jvm/miscellaneous/enums/Enum;>;
SourceFile: "Enum.java"

Autor: jgardo

Programista Java od 2013 roku. Interesuje się niskopoziomową Javą, ekosystemem Jvm i jego wydajnością. Co jednak nie przeszkadza w przywiązywaniu uwagi do czystości kodu w życiu codziennym ;) Aktualnie pracuje w PayU.

Jedna myśl na temat “Enum w JVM – szczegóły implementacyjne”

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *