在Java中,對Enum類型的序列化與其他對象類型的序列化有所不同,今天就來看看到底有什么不同。下面先來看下在Java中,我們定義的Enum在被編譯之后是長成什么樣子的。
Java代碼:
- public enum FruitEnum {
- APPLE, ORAGE
- }
上面的代碼定義了一個FruitEnum類型,是最簡單形式的,下面我們來看看編譯之后的字節碼。
字節碼:
- public final class com.taobao.tianxiao.FruitEnum extends java.lang.Enum
- ....
- ....
- ....
- {
- public static final com.taobao.tianxiao.FruitEnum APPLE;
- public static final com.taobao.tianxiao.FruitEnum ORAGE;
- static {};
- Code:
- Stack=4, Locals=0, Args_size=0
- 0: new #1; //class com/taobao/tianxiao/FruitEnum
- 3: dup
- 4: ldc #13; //String APPLE
- 6: iconst_0
- 7: invokespecial #14; //Method "<init>":(Ljava/lang/String;I)V
- 10: putstatic #18; //Field APPLE:Lcom/taobao/tianxiao/FruitEnum;
- 13: new #1; //class com/taobao/tianxiao/FruitEnum
- 16: dup
- 17: ldc #20; //String ORAGE
- 19: iconst_1
- 20: invokespecial #14; //Method "<init>":(Ljava/lang/String;I)V
- 23: putstatic #21; //Field ORAGE:Lcom/taobao/tianxiao/FruitEnum;
- 26: iconst_2
- 27: anewarray #1; //class com/taobao/tianxiao/FruitEnum
- 30: dup
- 31: iconst_0
- 32: getstatic #18; //Field APPLE:Lcom/taobao/tianxiao/FruitEnum;
- 35: aastore
- 36: dup
- 37: iconst_1
- 38: getstatic #21; //Field ORAGE:Lcom/taobao/tianxiao/FruitEnum;
- 41: aastore
- 42: putstatic #23; //Field ENUM$VALUES:[Lcom/taobao/tianxiao/FruitEnum;
- 45: return
- LineNumberTable:
- line 4: 0
- line 3: 26
- public static com.taobao.tianxiao.FruitEnum[] values();
- Code:
- Stack=5, Locals=3, Args_size=0
- 0: getstatic #23; //Field ENUM$VALUES:[Lcom/taobao/tianxiao/FruitEnum;
- 3: dup
- 4: astore_0
- 5: iconst_0
- 6: aload_0
- 7: arraylength
- 8: dup
- 9: istore_1
- 10: anewarray #1; //class com/taobao/tianxiao/FruitEnum
- 13: dup
- 14: astore_2
- 15: iconst_0
- 16: iload_1
- 17: invokestatic #31; //Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V
- 20: aload_2
- 21: areturn
- LineNumberTable:
- line 1: 0
- public static com.taobao.tianxiao.FruitEnum valueOf(java.lang.String);
- Code:
- Stack=2, Locals=1, Args_size=1
- 0: ldc #1; //class com/taobao/tianxiao/FruitEnum
- 2: aload_0
- 3: invokestatic #39; //Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
- 6: checkcast #1; //class com/taobao/tianxiao/FruitEnum
- 9: areturn
- LineNumberTable:
- line 1: 0
- }
上面的字節碼已經去掉的常量池部分,但是即便如此,在我們的源代碼中如此簡單的一個FruitEnum類,編譯器居然為我們產生了這么多的字節碼,哇哦~~~~~~~~
仔細地看這段代碼, 編譯器是在為我們創建一個類,這個類繼承自 java.lang.Enum ,有兩個公共的、靜態的、被聲明成final的屬性,它們的類型就是我們定義的FruitEnum。同時,編譯器還生成了一個靜態初始話器,就是字節碼中static{};這一行下面的代碼,其中的字節碼創建了兩個FruitEnum對象,同時分別賦值給APPLE和ORANGE這兩個屬性,調用的構造函數是定義在 java.lang.Enum中的protected Enum(String name, int ordinal)方法。在創建完成兩個FruitEnum對象並且分別賦值給APPLE和ORIGIN之后,還創建了一個名叫ENUM$VALUES的數組,然后把APPLE和ORIGIN按照定義的順序放如這個數組中。
除了這個靜態初始化器之外,編譯器還為我們生成了兩個靜態方法,values()和 valueOf(java.lang.String)方法。其中values()方法將ENUM$VALUES數組拷貝一份然后返回,而valueOf(java.lang.String)方法則會調用java.lang.Enum類中的valueOf方法,其作用是根據參數名找到對應的具體的枚舉對象,如果找不到的話會拋出一個IllegalArgumentException異常。
從上面的敘述可以看到,我們定義的枚舉類型,經過編譯器的處理最終會編程一個對象的定義,其中的枚舉變量其實就是類的靜態變量,因此Java中的枚舉類型其實是具有很多對象的特性的,只不過平時我們都不太用到,比如枚舉可以實現接口(不能繼承)、定義方法等等。為了保證枚舉類型像Java規范中所說的那樣,每一個枚舉類型極其定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java做了特殊的規定。原文如下(摘自Java的序列化規范):
引用
Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant's name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant's enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.
The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored--all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.
大概意思就是說,在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。下面我們來看看反序列化時候被調用的那個valueOf方法長什么樣子。
java代碼:
- public static <T extends Enum<T>> T valueOf(Class<T> enumType,
- String name) {
- T result = enumType.enumConstantDirectory().get(name);
- if (result != null)
- return result;
- if (name == null)
- throw new NullPointerException("Name is null");
- throw new IllegalArgumentException(
- "No enum const " + enumType +"." + name);
- }
從代碼中可以看到,代碼會嘗試從調用enumType這個Class對象的enumConstantDirectory()方法返回的map中獲取名字為name的枚舉對象,如果不存在就會拋出異常。再進一步跟到enumConstantDirectory()方法,就會發現到最后會以反射的方式調用enumType這個類型的values()靜態方法,也就是上面我們看到的編譯器為我們創建的那個方法,然后用返回結果填充enumType這個Class對象中的enumConstantDirectory屬性。
在了解了Java如何處理枚舉的定義以及序列化和反序列化枚舉類型之后,我們就需要在系統或者類庫升級時,對其中定義的枚舉類型多加注意,為了保持代碼上的兼容性,如果我們定義的枚舉類型有可能會被序列化保存(放到文件中、保存到數據庫中,進入分布式內存緩存中),那么我們是不能夠刪除原來枚舉類型中定義的任何枚舉對象的,否則程序在運行過程中,JVM就會抱怨找不到與某個名字對應的枚舉對象了。另外,在遠程方法調用過程中,如果我們發布的客戶端接口返回值中使用了枚舉類型,那么服務端在升級過程中就需要特別注意。如果在接口的返回結果的枚舉類型中添加了新的枚舉值,那就會導致仍然在使用老的客戶端的那些應用出現調用失敗的情況。因此,針對以上兩種情況,應該盡量避免使用枚舉,如果實在要用,也需要仔細設計,因為一旦用了枚舉,有可能會給后期維護帶來隱患。