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
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ą Lombok
ową….
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 Enum
a czyli:
public class Enum extends java.lang.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 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."":(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."":(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 #31 = Utf8 (Ljava/lang/String;II)V #32 = Utf8 this #33 = Utf8 Signature #34 = Utf8 (I)V #35 = Utf8 #36 = Utf8 ()V #37 = Utf8 Ljava/lang/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 // "":(Ljava/lang/String;I)V #46 = NameAndType #17:#18 // abc:I #47 = NameAndType #30:#31 // "":(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."":(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 "":(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 "":(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; SourceFile: "Enum.java"