1. 程式人生 > >java 列舉原始碼解析

java 列舉原始碼解析

應用場景

列舉通常用來列舉一個型別的有限例項集合,我們可以使用常量集來實現,jdk1.5添加了列舉(enum)支援,解決了常量集的一些缺陷

  • 常量集中的變數不會必然在指定的範圍內
  • 常量能夠提供的功能很少,難於使用
  • 常量意義不明確,沒有名字
  • 修改或增加列舉值後需要修改的程式碼多,不便於維護

關鍵字enum可以將一組具名的值的有限集合建立為一種新的型別,而這些具名的值可以作為常規的元件使用。

列舉原始碼

首先我們定義一個列舉類 Explore.java

public enum Explore {
    HERE, THERE
}  

然後編譯 javac Explore.java

,反編譯javap Explore,得到反編譯的結果:

Compiled from "Explore.java"
public final class Explore extends java.lang.Enum<Explore> {
    public static final Explore HERE;
    public static final Explore THERE;
    public static Explore[] values();
    public static Explore valueOf(java.lang.String);
    static
{}; }

我們看到當我們定義一個列舉,編譯器其實是為我們建立了一個繼承自Emum的類

  • 列舉例項對應新類中的static final 變數
  • 該類為 final型別,不可被繼承
  • 增加了兩個方法
    • valueOf(String) 從String構造列舉型別
    • values() 返回一個由列舉物件構成的陣列
  • 添加了一個靜態初始化器 static{},用來初始化列舉例項,和列舉例項陣列,也就是 values()返回陣列

Enum類

Enum作為列舉類的公共基類有以下的特點

  • 構造器私有(保護)
  • 關鍵域為ordinal來指示列舉物件被宣告的順序,name用來給出宣告時的合理描述,序列化時只輸出name屬性,用valueOf(String)通過名字來進行反序列化
  • 該類中的valueOf()方法和編譯器生成的valueOf方法簽名不同
  • 禁止了基礎的序列化方法,呼叫readObject()和writeObject()時丟擲異常
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
    private final String name;
    public final String name() {
        return name;
    }
    private final int ordinal;
    public final int ordinal() {
        return ordinal;
    }
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    public String toString() {
        return name;
    }
    public final boolean equals(Object other) {
        return this == other;
    }
    public final int hashCode() {
        return super.hashCode();
    }
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
    public final int compareTo(E o) {
        Enum other = (Enum) o;
        Enum self = this;
        if (self.getClass() != other.getClass() && // optimization
                self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }
    public final Class<E> getDeclaringClass() {
        Class clazz = getClass();
        Class zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? clazz : zuper;
    }
    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 constant " + enumType.getCanonicalName() + "." + name);
    }
    protected final void finalize() {
    }
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }
    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }
}  

序列化

序列化和反序列化保證了每一個列舉型別極其定義的列舉變數在JVM中都是唯一的

  • 在序列化的時候java僅僅是將列舉物件的name屬性輸出到結果中,反序列化的時候通過 java.lang.Enum的valueOf方法來根據名字查詢列舉物件
  • 通過私有化並且直接丟擲異常來禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,保證了序列化機制不會被定製
  • 嘗試從呼叫enumType這個Class物件的enumConstantDirectory()方法返回的map中獲取名字為name的列舉物件,如果不存在就會丟擲異常。再進一步跟到enumConstantDirectory()方法,就會發現到最後會以反射的方式呼叫enumType這個型別的values()靜態方法enumConstants = (T[])values.invoke(null);,也就是上面我們看到的編譯器為我們建立的那個方法,然後用返回結果填充enumType這個Class物件中的enumConstantDirectory屬性。由於每次反序列化都是獲取靜態陣列中的物件的引用,所以不會建立新的物件,從而保證了單例模式

NOTE
在瞭解了Java如何處理列舉的定義以及序列化和反序列化列舉型別之後,我們就需要在系統或者類庫升級時,對其中定義的列舉型別多加註意,為了保持程式碼上的相容性,如果我們定義的列舉型別有可能會被序列化儲存(放到檔案中、儲存到資料庫中,進入分散式記憶體快取中),那麼我們是不能夠刪除原來列舉型別中定義的任何列舉物件的,否則程式在執行過程中,JVM就會抱怨找不到與某個名字對應的列舉物件了。另外,在遠端方法呼叫過程中,如果我們釋出的客戶端介面返回值中使用了列舉型別,那麼服務端在升級過程中就需要特別注意。如果在介面的返回結果的列舉型別中添加了新的列舉值,那就會導致仍然在使用老的客戶端的那些應用出現呼叫失敗的情況。因此,針對以上兩種情況,應該儘量避免使用列舉,如果實在要用,也需要仔細設計,因為一旦用了列舉,有可能會給後期維護帶來隱患。

    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 constant " + enumType.getCanonicalName() + "." + name);
    }

一個測試序列化一致性的例子

本例中,我們通過socket將列舉型別寫入到網路中,然後在另一端用readObject()方法讀取,最後判斷反序列化後的物件與本地列舉物件的相等性

  • 如果 == 返回true,說明並沒有建立新的物件,確實保證了單例模式
  • 如果 equals()返回true,說明反序列化後只是語義相同,沒有保證單例模式
// 列舉型別定義
public enum WeekDayEnum {
    Mon(1), Tue(2), Wed(3), Thu(4), Fri(5), Sat(6), Sun(7);
    private int index;
    WeekDayEnum(int idx) {
        this.index = idx;
    }
    public int getIndex() {
        return index;
    }
}  
// 客戶端程式碼
public class EnumerationClient {
    public static void main(String... args) throws UnknownHostException, IOException {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("127.0.0.1", 8999));
        OutputStream os = socket.getOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(os);
        oos.writeObject(WeekDayEnum.Fri);
        oos.close();
        os.close();
        socket.close();
    }
}  
// 伺服器端程式碼
public class EnumerationServer {
    public static void main(String... args) throws IOException, ClassNotFoundException {
        ServerSocket server = new ServerSocket(8999);
        Socket socket = server.accept();
        InputStream is = socket.getInputStream();
        ObjectInputStream ois = new ObjectInputStream(is);
        WeekDayEnum day = (WeekDayEnum) ois.readObject();
        if (day == WeekDayEnum.Fri) {
            System.out.println("client Friday enum value is same as server's");
        } else if (day.equals(WeekDayEnum.Fri)) {
            System.out.println("client Friday enum value is equal to server's");
        } else {
            System.out.println("client Friday enum value is not same as server's");
        }
        ois.close();
        is.close();
        socket.close();
    }
} 

執行緒安全

由於列舉型別的物件是static,並且在static塊中初始化,所以由JVM的ClassLoader機制保證了執行緒安全性。

列舉的常見用法

主要API

public class TestEnum {
    public static void main(String[] args) {
        Fruit[] values = Fruit.values();
        for (Fruit fruit : values) {
            System.out.println(fruit);
            printEnum(fruit);
        }
    }
    public enum Fruit {
        APPLE, ORANGE, WATERMELON
    }
    public static void printEnum(Fruit fruit) {
        System.out.println(fruit + " ordinal:" + fruit.ordinal());
        System.out.println("compare to apple" + fruit.compareTo(Fruit.APPLE));
        System.out.println(fruit == Fruit.ORANGE);
        System.out.println(fruit.getDeclaringClass());
        System.out.println(fruit.name());
    }
}  

switch

java的switch語法,是通過jvm的tableswitch和lookupswitch兩個指令實現。java編譯器為switch語句編譯成一個區域性變數陣列,每個case對應一個數組的索引,指令的執行是通過不同的陣列索引找到不同的入口指令。所以原則上switch…case只能處理int型的變數。
enum能用在switch語句中,也是一個語法糖,我們知道所有列舉類的父類Enum中有一個private final int ordinal;,java編譯器檢測到switch語句中變數是一個列舉類,則會利用之前列舉類的ordinal屬性,編譯一個區域性變數陣列,後續在進行case分支比較的時候,就是簡單通過tableswitch或lookupswitch指令來進行跳轉,需要注意的一點:這個區域性變數陣列的構建過程是在編譯器在編譯階段完成的。

注意在switch中不需要用型別名來指定列舉物件(Single.RED),而是直接用型別名RED,在此時case語句不需要類限定字首,完全是java編譯器的限制(編譯器是不需要列舉類的字首,只需要列舉類編譯的static int[] $SWITCH_TABLE

enum Signal {
    GREEN, YELLOW, RED
}
public class TrafficLight {
    Signal color = Signal.RED;
    public void change() {
        switch (color) {
            case RED :
                color = Signal.GREEN;
                break;
            case YELLOW :
                color = Signal.RED;
                break;
            case GREEN :
                color = Signal.YELLOW;
                break;
        }
    }
}  

像正常類一樣擴充套件列舉型別

  • 定義私有建構函式
  • 自定義普通方法
  • 覆蓋Enum中的方法
  • 實現介面方法

由於java不支援多繼承,所有列舉型別都繼承自java.lang.Enum,所以enum型別只能實現介面,而不能繼承類

public enum Color implements Behaviour {
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);
    // 成員變數
    private String name;
    private int index;
    // 構造方法
    private Color(String name, int index) {
        this.name = name;
        this.index = index;
    }
    // 普通方法
    public static String getName(int index) {
        for (Color c : Color.values()) {
            if (c.index == index) {
                return c.name;
            }
        }
        return null;
    }
    // 介面方法
    @Override
    public void print() {
        System.out.println(this.index + ":" + this.name);
    }
    // 覆蓋方法
    @Override
    public String toString() {
        return this.index + "_" + this.name;
    }
}
interface Behaviour {
    void print();
}  

介面組織列舉型別

用介面來組織多層列舉

public class FoodTest {
    public static void main(String[] args) {
        Food f = Food.Coffee.BLACK_COFFEE;
        System.out.println(f);
    }
}
interface Food {
    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, LATTE, CAPPUCCINO
    }
    enum Dessert implements Food {
        FRUIT, CAKE, GELATO
    }
    int i = 1;
}  

列舉專用集合類

java.util.EnumSet和java.util.EnumMap是兩個列舉集合。EnumSet保證集合中的元素不重複;EnumMap中的key是enum型別,而value則可以是任意型別。集合類利用列舉型別的ordinal域來進行組織,用法和普通Map,Set類似,只是型別限定為Enum型別。

public enum Weeks {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURADAY, SUNDAY
} 
public class EnumSetTest {
    public static void main(String[] args) {
        EnumSet<Weeks> week = EnumSet.noneOf(Weeks.class);
        week.add(Weeks.MONDAY);
        System.out.println("EnumSet中的元素:" + week);
        week.remove(Weeks.MONDAY);
        System.out.println("EnumSet中的元素:" + week);
        week.addAll(EnumSet.complementOf(week));
        System.out.println("EnumSet中的元素:" + week);
        week.removeAll(EnumSet.range(Weeks.MONDAY, Weeks.THURSDAY));
        System.out.println("EnumSet中的元素:" + week);
    }
} 
public enum Course {
    ONE, TWO, THREE
}  
public class EnumMapTest {
    public static void main(String[] args) {
        EnumMap<Course, String> map = new EnumMap<Course, String>(Course.class);
        map.put(Course.ONE, "語文");
        map.put(Course.ONE, "政治");
        map.put(Course.TWO, "數學");
        map.put(Course.THREE, "英語");
        for (Entry<Course, String> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}