天天看點

Java進階系列——枚舉(Enums)

一、介紹

本系列文章的這一部分我們将會介紹Java 5版本之後引入的除泛型之外的另外一個強大特性:枚舉。可以将枚舉看成一種特殊的類,并且可以将注解看成一種特殊的接口。

枚舉的思想很簡單,也很友善:它代表了一組固定的常量值。實際上,枚舉經常用來設計一些狀态常量。比如,星期幾就是枚舉的一個最好例子,因為他們被限制在周一、周二、周三、周四、周五、 周六和周日。

二、枚舉作為特殊的類

在枚舉被引入Java語言之前,在Java中模拟一組固定值的正常方法是通過聲明一組常量。例如:

public class DaysOfTheWeekConstants {
    public static final int MONDAY = ;
    public static final int TUESDAY = ;
    public static final int WEDNESDAY = ;
    public static final int THURSDAY = ;
    public static final int FRIDAY = ;
    public static final int SATURDAY = ;
    public static final int SUNDAY = ;
}
           

雖然這種方法有效,但遠非理想的解決方案。主要是因為常量本身隻是int類型的值,而代碼中需要這些常量(而不是任意的int值)的每一個地方都應該被一直明确地記錄和斷言。從語義上來講,比如下面的這個方法示範所表現出來的就不符合類型安全的概念:

public boolean isWeekend( int day ) {
    return( day == SATURDAY || day == SUNDAY );
}
           

從邏輯角度去看,day參數應該是在DaysOfTheWeekConstants類中聲明的值之一。然而,如果沒有編寫額外的說明文檔(給後來的一些人閱讀),就不可能猜測到這一點。對于Java編譯器來說類似于isWeekend(100)的這種調用看起來完全是正确的并且不會引起任何顧慮。

此時枚舉就能解決這些問題。枚舉允許用類型化的值替換常量并在任何地方使用這些類型。讓我們使用枚舉重寫上面的方案。

public enum DaysOfTheWeek {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}
           

這裡将關鍵字class改成了enum并且這些可能的值在枚舉定義時被列舉出來。有差別的一部分就是被聲明在枚舉類(在我們的例子中是DaysOfTheWeek )中的每一個單獨的值都是一個執行個體。是以,每當枚舉被使用,Java編譯器都能夠進行類型檢查。比如:

public boolean isWeekend(DaysOfTheWeek day) {
    return(day == SATURDAY || day == SUNDAY);
}
           

請注意枚舉中的大寫命名法的使用是一個約定,但是如果你不這樣做也沒有誰能夠阻止你,但是最好還是遵守約定,這樣更有利于代碼的維護。

三、枚舉與執行個體字段(Enums and instance fields)

枚舉是一個特殊的類,是以它是可拓展的。這意味着他們可以有執行個體字段、構造器和方法(預設無參構造器不能夠被聲明并且所有的構造器必須被private修飾)。讓我們使用枚舉的執行個體和構造器添加一個isWeekend屬性。

public enum DaysOfTheWeekFields {
    MONDAY(false),
    TUESDAY(false),
    WEDNESDAY(false),
    THURSDAY(false),
    FRIDAY(false),
    SATURDAY(true),
    SUNDAY(true);

    private final boolean isWeekend;

    private DaysOfTheWeekFields( final boolean isWeekend ) {
        this.isWeekend = isWeekend;
    }

    public boolean isWeekend() {
        return isWeekend;
    }
}
           

我們看到,枚舉值隻是簡單的調用了構造器而并沒有要求使用new關鍵字。isWeekend()方法可以用來确定是否枚舉值代表工作日或者周末。比如:

public boolean isWeekend( DaysOfTheWeek day ) {
    return day.isWeekend();
}
           

Java中枚舉的執行個體字段有很大的用處。在正常的類聲明規則中,它們經常用來将一些額外的細節與每個值相關聯。

四、枚舉與接口(Enums and interfaces)

另外一個強大的特性,我們再次确認一下枚舉是是一個特殊的類,是以它能夠實作接口(然而枚舉不能夠繼承任何類)。比如,讓我們引入接口DayOfWeek。

interface DayOfWeek {
    boolean isWeekend();
}
           

然後使用接口實作代替正常執行個體字段的方式重寫前面的枚舉例子。

public enum DaysOfTheWeekInterfaces implements DayOfWeek {
    MONDAY() {
        @Override
        public boolean isWeekend() {
            return false;
        }
    },
    TUESDAY() {
        @Override
        public boolean isWeekend() {
            return false;
        }
    },
    WEDNESDAY() {
        @Override
        public boolean isWeekend() {
            return false;
        }
    },
    THURSDAY() {
        @Override
        public boolean isWeekend() {
            return false;
        }
    },
    FRIDAY() {
        @Override
        public boolean isWeekend() {
            return false;
        }
    },
    SATURDAY() {
        @Override
        public boolean isWeekend() {
            return true;
        }
    },
    SUNDAY() {
        @Override
        public boolean isWeekend() {
            return true;
        }
    };
}
           

我們實作接口的這種方式顯得代碼有些冗長,然而合并執行個體字段和接口實作可以解決這個問題,比如:

public enum DaysOfTheWeekFieldsInterfaces implements DayOfWeek {
    MONDAY( false ),
    TUESDAY( false ),
    WEDNESDAY( false ),
    THURSDAY( false ),
    FRIDAY( false ),
    SATURDAY( true ),
    SUNDAY( true );

    private final boolean isWeekend;

    private DaysOfTheWeekFieldsInterfaces(final boolean isWeekend){
        this.isWeekend = isWeekend;
    }

    @Override
    public boolean isWeekend() {
        return isWeekend;
    }
}
           

通過支援執行個體字段和接口,枚舉可以以更加面向對象的方式使用,進而帶來一定程度的抽象。

五、枚舉與泛型

在Java中,雖然咋一看并看不出來枚舉和泛型的關系,但是他們之間存在一種關系。Java中的每一個單獨的枚舉自動繼承自泛型類Enum<T>,在這裡T就是枚舉類型本身。Java編譯器在編譯時代表開發者做了這個轉換,拓展一下枚舉聲明public enum DaysOfTheWeek 如下:

public class DaysOfTheWeek extends Enum< DaysOfTheWeek > {
    // Other declarations here
}
           

這也就說明了為什麼枚舉可以實作接口但不能繼承其他類:因為它隐式的繼承自Enum<T>并且我們在使用對象的公共方法時已經讨論過,Java中不支援多繼承。

實際上每一個繼承自Enum<T>的枚舉允許定義泛型類、接口和方法,通過這種方式可以讓枚舉類型的執行個體參數化或者類型參數化。比如:

public<T extends Enum< ? >> void performAction(final T instance) {
    // Perform some action here
}
           

在上面的方法聲明中,類型T被約定為任意枚舉類型的執行個體并且Java編譯器将會對其做驗證。

六、枚舉方法

基礎類 Enum<T>為自動繼承它的枚舉執行個體提供了一些非常有用的方法。

方法 描述
String name() 傳回枚舉聲明聲明的枚舉常量的名稱
int ordinal() 傳回枚舉常量的次序(即枚舉聲明時的位置,初始常量配置設定的位置是0)

此外,Java編譯器為每個枚舉類型自動生成兩個更有用的靜态方法(讓我們将這個特殊的枚舉類型假設為T)。

方法 描述
T[] values() 傳回枚舉T所聲明的所有常量
T valueOf(String name) 傳回指定名稱的枚舉常量

在代碼中使用枚舉還有一個好處:可以使用switch/case文法。例如:

public void performAction( DaysOfTheWeek instance ) {
    switch( instance ) {
    case MONDAY:
        // Do something
    break;

    case TUESDAY:
        // Do something
    break;

    // Other enum constants here
    }
}
           

七、專用集合:EnumSet和EnumMap

和所有其他類一樣,枚舉的執行個體也可以和标準Java集合庫一起使用。然而,某些集合類型針對枚舉做了優化,并且在大多數情況下推薦使用這些優化過後的集合代替通用的集合。

本節我們簡單了解一下兩個專用的集合:EnumSet<T>和EnumMap<T, ?>。這兩個集合都非常容易使用。

我們首先來看一下EnumSet<T>集合。EnumSet<T>集合是正常的集合優化過後高效存儲枚舉類型的一個集合,EnumSet<T>不能夠使用構造器進行執行個體化,但是它提供了很多非常有用的工廠方法。

比如,allOf工廠方法建立的EnumSet<T>執行個體就包含了所有枚舉類型所枚舉的常量:

noneOf工廠方法建立的是一個空的EnumSet<T>執行個體:

使用of工廠方法,可以指定枚舉類型中那些枚舉常量應該包含在EnumSet<T>中:

final Set< DaysOfTheWeek > enumSetSome = EnumSet.of(
    DaysOfTheWeek.SUNDAY,
    DaysOfTheWeek.SATURDAY
);
           

EnumMap<T, ?>是最接近于一般的map的,唯一的不同就是EnumMap<T, ?>的key是枚舉類型的枚舉常量。比如;

final Map<DaysOfTheWeek, String> enumMap = new EnumMap<>(DaysOfTheWeek.class);
enumMap.put(DaysOfTheWeek.MONDAY, "Lundi");
enumMap.put(DaysOfTheWeek.TUESDAY, "Mardi");
           

注意,和大多數集合實作一樣,EnumSet<T>和EnumMap<T, ?>不是線程安全的是以不能在多線程環境下使用。

八、何時使用枚舉

自Java 5釋出以來,在解決一些固定常量集合的問題上枚舉成為唯一首選和推薦的一種方式。不僅是因為它們是強類型,同時它們是可拓展并被目前的很多庫和架構所支援。