秒懂Java枚舉類型(enum)
理解枚舉類型
枚舉類型是Java 5中新增特性的一部分,它是一種特殊的數據類型,之所以特殊是因為它既是一種類(class)類型卻又比類類型多瞭些特殊的約束,但是這些約束的存在也造就瞭枚舉類型的簡潔性、安全性以及便捷性。下面先來看看什麼是枚舉?如何定義枚舉?
枚舉的定義
上述的常量定義常量的方式稱為int枚舉模式,這樣的定義方式並沒有什麼錯,但它存在許多不足,如在類型安全和使用方便性上並沒有多少好處,如果存在定義int值相同的變量,混淆的幾率還是很大的,編譯器也不會提出任何警告,因此這種方式在枚舉出現後並不提倡,現在我們利用枚舉類型來重新定義上述的常量,同時也感受一把枚舉定義的方式,如下定義周一到周日的常量
public class DayDemo { public static final int MONDAY =1; public static final int TUESDAY=2; public static final int WEDNESDAY=3; public static final int THURSDAY=4; public static final int FRIDAY=5; public static final int SATURDAY=6; public static final int SUNDAY=7; }
相當簡潔,在定義枚舉類型時我們使用的關鍵字是enum,與class關鍵字類似,隻不過前者是定義枚舉類型,後者是定義類類型。枚舉類型Day中分別定義瞭從周一到周日的值,這裡要註意,值一般是大寫的字母,多個值之間以逗號分隔。同時我們應該知道的是枚舉類型可以像類(class)類型一樣,定義為一個單獨的文件,當然也可以定義在其他類內部,更重要的是枚舉常量在類型安全性和便捷性都很有保證,如果出現類型問題編譯器也會提示我們改進,但務必記住枚舉表示的類型其取值是必須有限的,也就是說每個值都是可以枚舉出來的,比如上述描述的一周共有七天。那麼該如何使用呢?如下:
//枚舉類型,使用關鍵字enum enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
就像上述代碼那樣,直接引用枚舉的值即可,這便是枚舉類型的最簡單模型。
枚舉實現的原理
我們大概瞭解瞭枚舉類型的定義與簡單使用後,現在有必要來瞭解一下枚舉類型的基本實現原理。實際上在使用關鍵字enum創建枚舉類型並編譯後,編譯器會為我們生成一個相關的類,這個類繼承瞭Java API中的java.lang.Enum類,也就是說通過關鍵字enum創建枚舉類型在編譯後事實上也是一個類類型而且該類繼承自java.lang.Enum類。下面我們編譯前面定義的EnumDemo.java並查看生成的class文件來驗證這個結論:
javac EnumDemo.java Day.class EnumDemo.class EnumDemo.java
利用javac編譯前面定義的EnumDemo.java文件後分別生成瞭Day.class和EnumDemo.class文件,而Day.class就是枚舉類型,這也就驗證前面所說的使用關鍵字enum定義枚舉類型並編譯後,編譯器會自動幫助我們生成一個與枚舉相關的類。我們再來看看反編譯Day.class文件:
//反編譯Day.class final class Day extends Enum { //編譯器為我們添加的靜態的values()方法 public static Day[] values() { return (Day[])$VALUES.clone(); } //編譯器為我們添加的靜態的valueOf()方法,註意間接調用瞭Enum也類的valueOf方法 public static Day valueOf(String s) { return (Day)Enum.valueOf(com/zejian/enumdemo/Day, s); } //私有構造函數 private Day(String s, int i) { super(s, i); } //前面定義的7種枚舉實例 public static final Day MONDAY; public static final Day TUESDAY; public static final Day WEDNESDAY; public static final Day THURSDAY; public static final Day FRIDAY; public static final Day SATURDAY; public static final Day SUNDAY; private static final Day $VALUES[]; static { //實例化枚舉實例 MONDAY = new Day("MONDAY", 0); TUESDAY = new Day("TUESDAY", 1); WEDNESDAY = new Day("WEDNESDAY", 2); THURSDAY = new Day("THURSDAY", 3); FRIDAY = new Day("FRIDAY", 4); SATURDAY = new Day("SATURDAY", 5); SUNDAY = new Day("SUNDAY", 6); $VALUES = (new Day[] { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }); } }
從反編譯的代碼可以看出編譯器確實幫助我們生成瞭一個Day類(註意該類是final類型的,將無法被繼承)而且該類繼承自java.lang.Enum類,該類是一個抽象類(稍後我們會分析該類中的主要方法),除此之外,編譯器還幫助我們生成瞭7個Day類型的實例對象分別對應枚舉中定義的7個日期,這也充分說明瞭我們前面使用關鍵字enum定義的Day類型中的每種日期枚舉常量也是實實在在的Day實例對象,隻不過代表的內容不一樣而已。註意編譯器還為我們生成瞭兩個靜態方法,分別是values()和 valueOf(),稍後會分析它們的用法,到此我們也就明白瞭,使用關鍵字enum定義的枚舉類型,在編譯期後,也將轉換成為一個實實在在的類,而在該類中,會存在每個在枚舉類型中定義好變量的對應實例對象,如上述的MONDAY枚舉類型對應public static final Day MONDAY;
,同時編譯器會為該類創建兩個方法,分別是values()和valueOf()。ok~,到此相信我們對枚舉的實現原理也比較清晰,下面我們深入瞭解一下java.lang.Enum類以及values()和valueOf()的用途。
枚舉的常用方法
Enum抽象類常用方法
Enum是所有 Java 語言枚舉類型的公共基本類(註意Enum是抽象類),以下是它的常見方法:
返回類型 | 方法名稱 | 方法說明 |
---|---|---|
int | compareTo(E o) | 比較此枚舉與指定對象的順序 |
boolean | equals(Object other) | 當指定對象等於此枚舉常量時,返回 true。 |
Class<?> | getDeclaringClass() | 返回與此枚舉常量的枚舉類型相對應的 Class 對象 |
String | name() | 返回此枚舉常量的名稱,在其枚舉聲明中對其進行聲明 |
int | ordinal() | 返回枚舉常量的序數(它在枚舉聲明中的位置,其中初始常量序數為零) |
String | toString() | 返回枚舉常量的名稱,它包含在聲明中 |
static<T extends Enum<T>> T | static valueOf(Class<T> enumType, String name) |
返回帶指定名稱的指定枚舉類型的枚舉常量。 |
這裡主要說明一下ordinal()
方法,該方法獲取的是枚舉變量在枚舉類中聲明的順序,下標從0開始,如日期中的MONDAY在第一個位置,那麼MONDAY的ordinal值就是0,如果MONDAY的聲明位置發生變化,那麼ordinal方法獲取到的值也隨之變化,註意在大多數情況下我們都不應該首先使用該方法,畢竟它總是變幻莫測的。compareTo(E o)
方法則是比較枚舉的大小,註意其內部實現是根據每個枚舉的ordinal值大小進行比較的。name()
方法與toString()
幾乎是等同的,都是輸出變量的字符串形式。至於valueOf(Class<T> enumType, String name)
方法則是根據枚舉類的Class對象和枚舉名稱獲取枚舉常量,註意該方法是靜態的,後面在枚舉單例時,我們還會詳細分析該方法,下面的代碼演示瞭上述方法:
public class EnumDemo { public static void main(String[] args){ //創建枚舉數組 Day[] days=new Day[]{Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY, Day.THURSDAY, Day.FRIDAY, Day.SATURDAY, Day.SUNDAY}; for (int i = 0; i <days.length ; i++) { System.out.println("day["+i+"].ordinal():"+days[i].ordinal()); } System.out.println("-------------------------------------"); //通過compareTo方法比較,實際上其內部是通過ordinal()值比較的 System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[1])); System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[2])); //獲取該枚舉對象的Class對象引用,當然也可以通過getClass方法 Class<?> clazz = days[0].getDeclaringClass(); System.out.println("clazz:"+clazz); System.out.println("-------------------------------------"); //name() System.out.println("days[0].name():"+days[0].name()); System.out.println("days[1].name():"+days[1].name()); System.out.println("days[2].name():"+days[2].name()); System.out.println("days[3].name():"+days[3].name()); System.out.println("-------------------------------------"); System.out.println("days[0].toString():"+days[0].toString()); System.out.println("days[1].toString():"+days[1].toString()); System.out.println("days[2].toString():"+days[2].toString()); System.out.println("days[3].toString():"+days[3].toString()); System.out.println("-------------------------------------"); Day d=Enum.valueOf(Day.class,days[0].name()); Day d2=Day.valueOf(Day.class,days[0].name()); System.out.println("d:"+d); System.out.println("d2:"+d2); } /** 執行結果: day[0].ordinal():0 day[1].ordinal():1 day[2].ordinal():2 day[3].ordinal():3 day[4].ordinal():4 day[5].ordinal():5 day[6].ordinal():6 ------------------------------------- days[0].compareTo(days[1]):-1 days[0].compareTo(days[1]):-2 clazz:class com.zejian.enumdemo.Day ------------------------------------- days[0].name():MONDAY days[1].name():TUESDAY days[2].name():WEDNESDAY days[3].name():THURSDAY ------------------------------------- days[0].toString():MONDAY days[1].toString():TUESDAY days[2].toString():WEDNESDAY days[3].toString():THURSDAY ------------------------------------- d:MONDAY d2:MONDAY */ } enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
到此對於抽象類Enum類的基本內容就介紹完瞭,這裡提醒大傢一點,Enum類內部會有一個構造函數,該構造函數隻能有編譯器調用,我們是無法手動操作的,不妨看看Enum類的主要源碼:
//實現瞭Comparable 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; } //比較的是ordinal值 public final int compareTo(E o) { Enum<?> other = (Enum<?>)o; Enum<E> self = this; if (self.getClass() != other.getClass() && // optimization self.getDeclaringClass() != other.getDeclaringClass()) throw new ClassCastException(); return self.ordinal - other.ordinal;//根據ordinal值比較大小 } @SuppressWarnings("unchecked") public final Class<E> getDeclaringClass() { //獲取class對象引用,getClass()是Object的方法 Class<?> clazz = getClass(); //獲取父類Class對象引用 Class<?> zuper = clazz.getSuperclass(); return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper; } public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { //enumType.enumConstantDirectory()獲取到的是一個map集合,key值就是name值,value則是枚舉變量值 //enumConstantDirectory是class對象內部的方法,根據class對象獲取一個map集合的值 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); } //.....省略其他沒用的方法 }
通過Enum源碼,可以知道,Enum實現瞭Comparable接口,這也是可以使用compareTo比較的原因,當然Enum構造函數也是存在的,該函數隻能由編譯器調用,畢竟我們隻能使用enum關鍵字定義枚舉,其他事情就放心交給編譯器吧。
//由編譯器調用 protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }
編譯器生成的Values方法與ValueOf方法
values()方法和valueOf(String name)方法是編譯器生成的static方法,因此從前面的分析中,在Enum類中並沒出現values()方法,但valueOf()方法還是有出現的,隻不過編譯器生成的valueOf()方法需傳遞一個name參數,而Enum自帶的靜態方法valueOf()則需要傳遞兩個方法,從前面反編譯後的代碼可以看出,編譯器生成的valueOf方法最終還是調用瞭Enum類的valueOf方法,下面通過代碼來演示這兩個方法的作用:
Day[] days2 = Day.values(); System.out.println("day2:"+Arrays.toString(days2)); Day day = Day.valueOf("MONDAY"); System.out.println("day:"+day); /** 輸出結果: day2:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY] day:MONDAY */
從結果可知道,values()方法的作用就是獲取枚舉類中的所有變量,並作為數組返回,而valueOf(String name)方法與Enum類中的valueOf方法的作用類似根據名稱獲取枚舉變量,隻不過編譯器生成的valueOf方法更簡潔些隻需傳遞一個參數。這裡我們還必須註意到,由於values()方法是由編譯器插入到枚舉類中的static方法,所以如果我們將枚舉實例向上轉型為Enum,那麼values()方法將無法被調用,因為Enum類中並沒有values()方法,valueOf()方法也是同樣的道理,註意是一個參數的。
//正常使用 Day[] ds=Day.values(); //向上轉型Enum Enum e = Day.MONDAY; //無法調用,沒有此方法 //e.values();
枚舉與Class對象
上述我們提到當枚舉實例向上轉型為Enum類型後,values()方法將會失效,也就無法一次性獲取所有枚舉實例變量,但是由於Class對象的存在,即使不使用values()方法,還是有可能一次獲取到所有枚舉實例變量的,在Class對象中存在如下方法:
返回類型 | 方法名稱 | 方法說明 |
---|---|---|
T[] | getEnumConstants() | 返回該枚舉類型的所有元素,如果Class對象不是枚舉類型,則返回null。 |
boolean | isEnum() | 當且僅當該類聲明為源代碼中的枚舉時返回 true |
因此通過getEnumConstants()方法,同樣可以輕而易舉地獲取所有枚舉實例變量下面通過代碼來演示這個功能:
//正常使用 Day[] ds=Day.values(); //向上轉型Enum Enum e = Day.MONDAY; //無法調用,沒有此方法 //e.values(); //獲取class對象引用 Class<?> clasz = e.getDeclaringClass(); if(clasz.isEnum()) { Day[] dsz = (Day[]) clasz.getEnumConstants(); System.out.println("dsz:"+Arrays.toString(dsz)); } /** 輸出結果: dsz:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY] */
正如上述代碼所展示,通過Enum的class對象的getEnumConstants方法,我們仍能一次性獲取所有的枚舉實例常量。
枚舉的進階用法
在前面的分析中,我們都是基於簡單枚舉類型的定義,也就是在定義枚舉時隻定義瞭枚舉實例類型,並沒定義方法或者成員變量,實際上使用關鍵字enum定義的枚舉類,除瞭不能使用繼承(因為編譯器會自動為我們繼承Enum抽象類而Java隻支持單繼承,因此枚舉類是無法手動實現繼承的),可以把enum類當成常規類,也就是說我們可以向enum類中添加方法和變量,甚至是mian方法,下面就來感受一把。
向enum類添加方法與自定義構造函數
重新定義一個日期枚舉類,帶有desc成員變量描述該日期的對於中文描述,同時定義一個getDesc方法,返回中文描述內容,自定義私有構造函數,在聲明枚舉實例時傳入對應的中文描述,代碼如下:
public enum Day2 { MONDAY("星期一"), TUESDAY("星期二"), WEDNESDAY("星期三"), THURSDAY("星期四"), FRIDAY("星期五"), SATURDAY("星期六"), SUNDAY("星期日");//記住要用分號結束 private String desc;//中文描述 /** * 私有構造,防止被外部調用 * @param desc */ private Day2(String desc){ this.desc=desc; } /** * 定義方法,返回描述,跟常規類的定義沒區別 * @return */ public String getDesc(){ return desc; } public static void main(String[] args){ for (Day2 day:Day2.values()) { System.out.println("name:"+day.name()+ ",desc:"+day.getDesc()); } } /** 輸出結果: name:MONDAY,desc:星期一 name:TUESDAY,desc:星期二 name:WEDNESDAY,desc:星期三 name:THURSDAY,desc:星期四 name:FRIDAY,desc:星期五 name:SATURDAY,desc:星期六 name:SUNDAY,desc:星期日 */ }
從上述代碼可知,在enum類中確實可以像定義常規類一樣聲明變量或者成員方法。但是我們必須註意到,如果打算在enum類中定義方法,務必在聲明完枚舉實例後使用分號分開,倘若在枚舉實例前定義任何方法,編譯器都將會報錯,無法編譯通過,同時即使自定義瞭構造函數且enum的定義結束,我們也永遠無法手動調用構造函數創建枚舉實例,畢竟這事隻能由編譯器執行。
關於覆蓋enum類方法
既然enum類跟常規類的定義沒什麼區別(實際上enum還是有些約束的),那麼覆蓋父類的方法也不會是什麼難說,可惜的是父類Enum中的定義的方法隻有toString方法沒有使用final修飾,因此隻能覆蓋toString方法,如下通過覆蓋toString省去瞭getDesc方法:
public enum Day2 { MONDAY("星期一"), TUESDAY("星期二"), WEDNESDAY("星期三"), THURSDAY("星期四"), FRIDAY("星期五"), SATURDAY("星期六"), SUNDAY("星期日");//記住要用分號結束 private String desc;//中文描述 /** * 私有構造,防止被外部調用 * @param desc */ private Day2(String desc){ this.desc=desc; } /** * 覆蓋 * @return */ @Override public String toString() { return desc; } public static void main(String[] args){ for (Day2 day:Day2.values()) { System.out.println("name:"+day.name()+ ",desc:"+day.toString()); } } /** 輸出結果: name:MONDAY,desc:星期一 name:TUESDAY,desc:星期二 name:WEDNESDAY,desc:星期三 name:THURSDAY,desc:星期四 name:FRIDAY,desc:星期五 name:SATURDAY,desc:星期六 name:SUNDAY,desc:星期日 */ }
enum類中定義抽象方法
與常規抽象類一樣,enum類允許我們為其定義抽象方法,然後使每個枚舉實例都實現該方法,以便產生不同的行為方式,註意abstract關鍵字對於枚舉類來說並不是必須的如下:
public enum EnumDemo3 { FIRST{ @Override public String getInfo() { return "FIRST TIME"; } }, SECOND{ @Override public String getInfo() { return "SECOND TIME"; } } ; /** * 定義抽象方法 * @return */ public abstract String getInfo(); //測試 public static void main(String[] args){ System.out.println("F:"+EnumDemo3.FIRST.getInfo()); System.out.println("S:"+EnumDemo3.SECOND.getInfo()); /** 輸出結果: F:FIRST TIME S:SECOND TIME */ } }
通過這種方式就可以輕而易舉地定義每個枚舉實例的不同行為方式。我們可能註意到,enum類的實例似乎表現出瞭多態的特性,可惜的是枚舉類型的實例終究不能作為類型傳遞使用,就像下面的使用方式,編譯器是不可能答應的:
//無法通過編譯,畢竟EnumDemo3.FIRST是個實例對象 public void text(EnumDemo3.FIRST instance){ }
在枚舉實例常量中定義抽象方法
enum類與接口
由於Java單繼承的原因,enum類並不能再繼承其它類,但並不妨礙它實現接口,因此enum類同樣是可以實現多接口的,如下:
interface food{ void eat(); } interface sport{ void run(); } public enum EnumDemo2 implements food ,sport{ FOOD, SPORT, ; //分號分隔 @Override public void eat() { System.out.println("eat....."); } @Override public void run() { System.out.println("run....."); } }
有時候,我們可能需要對一組數據進行分類,比如進行食物菜單分類而且希望這些菜單都屬於food類型,appetizer(開胃菜)、mainCourse(主菜)、dessert(點心)、Coffee等,每種分類下有多種具體的菜式或食品,此時可以利用接口來組織,如下(代碼引用自Thinking in Java):
public interface Food { enum Appetizer implements Food { SALAD, SOUP, SPRING_ROLLS; } enum MainCourse implements Food { LASAGNE, BURRITO, PAD_THAI, LENTILS, HUMMOUS, VINDALOO; } enum Dessert implements Food { TIRAMISU, GELATO, BLACK_FOREST_CAKE, FRUIT, CREME_CARAMEL; } enum Coffee implements Food { BLACK_COFFEE, DECAF_COFFEE, ESPRESSO, LATTE, CAPPUCCINO, TEA, HERB_TEA; } } public class TypeOfFood { public static void main(String[] args) { Food food = Appetizer.SALAD; food = MainCourse.LASAGNE; food = Dessert.GELATO; food = Coffee.CAPPUCCINO; } }
通過這種方式可以很方便組織上述的情景,同時確保每種具體類型的食物也屬於Food,現在我們利用一個枚舉嵌套枚舉的方式,把前面定義的菜譜存放到一個Meal菜單中,通過這種方式就可以統一管理菜單的數據瞭。
public enum Meal{ APPETIZER(Food.Appetizer.class), MAINCOURSE(Food.MainCourse.class), DESSERT(Food.Dessert.class), COFFEE(Food.Coffee.class); private Food[] values; private Meal(Class<? extends Food> kind) { //通過class對象獲取枚舉實例 values = kind.getEnumConstants(); } public interface Food { enum Appetizer implements Food { SALAD, SOUP, SPRING_ROLLS; } enum MainCourse implements Food { LASAGNE, BURRITO, PAD_THAI, LENTILS, HUMMOUS, VINDALOO; } enum Dessert implements Food { TIRAMISU, GELATO, BLACK_FOREST_CAKE, FRUIT, CREME_CARAMEL; } enum Coffee implements Food { BLACK_COFFEE, DECAF_COFFEE, ESPRESSO, LATTE, CAPPUCCINO, TEA, HERB_TEA; } } }
枚舉與switch
關於枚舉與switch是個比較簡單的話題,使用switch進行條件判斷時,條件參數一般隻能是整型,字符型。而枚舉型確實也被switch所支持,在java 1.7後switch也對字符串進行瞭支持。這裡我們簡單看一下switch與枚舉類型的使用:
enum Color {GREEN,RED,BLUE} public class EnumDemo4 { public static void printName(Color color){ switch (color){ case BLUE: //無需使用Color進行引用 System.out.println("藍色"); break; case RED: System.out.println("紅色"); break; case GREEN: System.out.println("綠色"); break; } } public static void main(String[] args){ printName(Color.BLUE); printName(Color.RED); printName(Color.GREEN); //藍色 //紅色 //綠色 } }
需要註意的是使用在於switch條件進行結合使用時,無需使用Color引用。
枚舉與單例模式
單例模式可以說是最常使用的設計模式瞭,它的作用是確保某個類隻有一個實例,自行實例化並向整個系統提供這個實例。在實際應用中,線程池、緩存、日志對象、對話框對象常被設計成單例,總之,選擇單例模式就是為瞭避免不一致狀態,下面我們將會簡單說明單例模式的幾種主要編寫方式,從而對比出使用枚舉實現單例模式的優點。首先看看餓漢式的單例模式:
/** * 餓漢式(基於classloder機制避免瞭多線程的同步問題) */ public class SingletonHungry { private static SingletonHungry instance = new SingletonHungry(); private SingletonHungry() { } public static SingletonHungry getInstance() { return instance; } }
顯然這種寫法比較簡單,但問題是無法做到延遲創建對象,事實上如果該單例類涉及資源較多,創建比較耗時間時,我們更希望它可以盡可能地延遲加載,從而減小初始化的負載,於是便有瞭如下的懶漢式單例:
/** * Created by wuzejian on 2017/5/9.. * 懶漢式單例模式(適合多線程安全) */ public class SingletonLazy { private static volatile SingletonLazy instance; private SingletonLazy() { } public static synchronized SingletonLazy getInstance() { if (instance == null) { instance = new SingletonLazy(); } return instance; } }
這種寫法能夠在多線程中很好的工作避免同步問題,同時也具備lazy loading機制,遺憾的是,由於synchronized的存在,效率很低,在單線程的情景下,完全可以去掉synchronized,為瞭兼顧效率與性能問題,改進後代碼如下:
public class Singleton { private static volatile Singleton singleton = null; private Singleton(){} public static Singleton getSingleton(){ if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
這種編寫方式被稱為“雙重檢查鎖”,主要在getSingleton()方法中,進行兩次null檢查。這樣可以極大提升並發度,進而提升性能。畢竟在單例中new的情況非常少,絕大多數都是可以並行的讀操作,因此在加鎖前多進行一次null檢查就可以減少絕大多數的加鎖操作,也就提高瞭執行效率。但是必須註意的是volatile關鍵字,該關鍵字有兩層語義。第一層語義是可見性,可見性是指在一個線程中對該變量的修改會馬上由工作內存(Work Memory)寫回主內存(Main Memory),所以其它線程會馬上讀取到已修改的值,關於工作內存和主內存可簡單理解為高速緩存(直接與CPU打交道)和主存(日常所說的內存條),註意工作內存是線程獨享的,主存是線程共享的。volatile的第二層語義是禁止指令重排序優化,我們寫的代碼(特別是多線程代碼),由於編譯器優化,在實際執行的時候可能與我們編寫的順序不同。編譯器隻保證程序執行結果與源代碼相同,卻不保證實際指令的順序與源代碼相同,這在單線程並沒什麼問題,然而一旦引入多線程環境,這種亂序就可能導致嚴重問題。volatile關鍵字就可以從語義上解決這個問題,值得關註的是volatile的禁止指令重排序優化功能在Java 1.5後才得以實現,因此1.5前的版本仍然是不安全的,即使使用瞭volatile關鍵字。或許我們可以利用靜態內部類來實現更安全的機制,靜態內部類單例模式如下:
/** * Created by wuzejian on 2017/5/9. * 靜態內部類 */ public class SingletonInner { private static class Holder { private static SingletonInner singleton = new SingletonInner(); } private SingletonInner(){} public static SingletonInner getSingleton(){ return Holder.singleton; } }
正如上述代碼所展示的,我們把Singleton實例放到一個靜態內部類中,這樣可以避免瞭靜態實例在Singleton類的加載階段(類加載過程的其中一個階段的,此時隻創建瞭Class對象,關於Class對象可以看博主另外一篇博文,深入理解Java類型信息(Class對象)與反射機制)就創建對象,畢竟靜態變量初始化是在SingletonInner類初始化時觸發的,並且由於靜態內部類隻會被加載一次,所以這種寫法也是線程安全的。從上述4種單例模式的寫法中,似乎也解決瞭效率與懶加載的問題,但是它們都有兩個共同的缺點:
序列化可能會破壞單例模式,比較每次反序列化一個序列化的對象實例時都會創建一個新的實例,解決方案如下:
//測試例子(四種寫解決方式雷同) public class Singleton implements java.io.Serializable { public static Singleton INSTANCE = new Singleton(); protected Singleton() { } //反序列時直接返回當前INSTANCE private Object readResolve() { return INSTANCE; } }
使用反射強行調用私有構造器,解決方式可以修改構造器,讓它在創建第二個實例的時候拋異常,如下:
public static Singleton INSTANCE = new Singleton(); private static volatile boolean flag = true; private Singleton(){ if(flag){ flag = false; }else{ throw new RuntimeException("The instance already exists !"); } }
如上所述,問題確實也得到瞭解決,但問題是我們為此付出瞭不少努力,即添加瞭不少代碼,還應該註意到如果單例類維持瞭其他對象的狀態時還需要使他們成為transient的對象,這種就更復雜瞭,那有沒有更簡單更高效的呢?當然是有的,那就是枚舉單例瞭,先來看看如何實現:
/** * 枚舉單利 */ public enum SingletonEnum { INSTANCE; private String name; public String getName(){ return name; } public void setName(String name){ this.name = name; } }
代碼相當簡潔,我們也可以像常規類一樣編寫enum類,為其添加變量和方法,訪問方式也更簡單,使用SingletonEnum.INSTANCE
進行訪問,這樣也就避免調用getInstance方法,更重要的是使用枚舉單例的寫法,我們完全不用考慮序列化和反射的問題。枚舉序列化是由jvm保證的,每一個枚舉類型和定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java做瞭特殊的規定:在序列化時Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定制的並禁用瞭writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,從而保證瞭枚舉實例的唯一性,這裡我們不妨再次看看Enum類的valueOf方法:
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); }
實際上通過調用enumType(Class對象的引用)的enumConstantDirectory方法獲取到的是一個Map集合,在該集合中存放瞭以枚舉name為key和以枚舉實例變量為value的Key&Value數據,因此通過name的值就可以獲取到枚舉實例,看看enumConstantDirectory方法源碼:
Map<String, T> enumConstantDirectory() { if (enumConstantDirectory == null) { //getEnumConstantsShared最終通過反射調用枚舉類的values方法 T[] universe = getEnumConstantsShared(); if (universe == null) throw new IllegalArgumentException( getName() + " is not an enum type"); Map<String, T> m = new HashMap<>(2 * universe.length); //map存放瞭當前enum類的所有枚舉實例變量,以name為key值 for (T constant : universe) m.put(((Enum<?>)constant).name(), constant); enumConstantDirectory = m; } return enumConstantDirectory; } private volatile transient Map<String, T> enumConstantDirectory = null;
到這裡我們也就可以看出枚舉序列化確實不會重新創建新實例,jvm保證瞭每個枚舉實例變量的唯一性。再來看看反射到底能不能創建枚舉,下面試圖通過反射獲取構造器並創建枚舉
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { //獲取枚舉類的構造函數(前面的源碼已分析過) Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class); constructor.setAccessible(true); //創建枚舉 SingletonEnum singleton=constructor.newInstance("otherInstance",9); }
執行報錯
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at zejian.SingletonEnum.main(SingletonEnum.java:38) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
顯然告訴我們不能使用反射創建枚舉類,這是為什麼呢?不妨看看newInstance方法源碼:
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } //這裡判斷Modifier.ENUM是不是枚舉修飾符,如果是就拋異常 if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
源碼很瞭然,確實無法使用反射創建枚舉實例,也就是說明瞭創建枚舉實例隻有編譯器能夠做到而已。顯然枚舉單例模式確實是很不錯的選擇,因此我們推薦使用它。但是這總不是萬能的,對於android平臺這個可能未必是最好的選擇,在android開發中,內存優化是個大塊頭,而使用枚舉時占用的內存常常是靜態變量的兩倍還多,因此android官方在內存優化方面給出的建議是盡量避免在android中使用enum。但是不管如何,關於單例,我們總是應該記住:線程安全,延遲加載,序列化與反序列化安全,反射安全是很重重要的。
EnumMap
EnumMap基本用法
先思考這樣一個問題,現在我們有一堆size大小相同而顏色不同的數據,需要統計出每種顏色的數量是多少以便將數據錄入倉庫,定義如下枚舉用於表示顏色Color:
enum Color { GREEN,RED,BLUE,YELLOW }
import java.util.*; public class EnumMapDemo { public static void main(String[] args){ List<Clothes> list = new ArrayList<>(); list.add(new Clothes("C001",Color.BLUE)); list.add(new Clothes("C002",Color.YELLOW)); list.add(new Clothes("C003",Color.RED)); list.add(new Clothes("C004",Color.GREEN)); list.add(new Clothes("C005",Color.BLUE)); list.add(new Clothes("C006",Color.BLUE)); list.add(new Clothes("C007",Color.RED)); list.add(new Clothes("C008",Color.YELLOW)); list.add(new Clothes("C009",Color.YELLOW)); list.add(new Clothes("C010",Color.GREEN)); //方案1:使用HashMap Map<String,Integer> map = new HashMap<>(); for (Clothes clothes:list){ String colorName=clothes.getColor().name(); Integer count = map.get(colorName); if(count!=null){ map.put(colorName,count+1); }else { map.put(colorName,1); } } System.out.println(map.toString()); System.out.println("---------------"); //方案2:使用EnumMap Map<Color,Integer> enumMap=new EnumMap<>(Color.class); for (Clothes clothes:list){ Color color=clothes.getColor(); Integer count = enumMap.get(color); if(count!=null){ enumMap.put(color,count+1); }else { enumMap.put(color,1); } } System.out.println(enumMap.toString()); } /** 輸出結果: {RED=2, BLUE=3, YELLOW=3, GREEN=2} --------------- {GREEN=2, RED=2, BLUE=3, YELLOW=3} */ }
代碼比較簡單,我們使用兩種解決方案,一種是HashMap,一種EnumMap,雖然都統計出瞭正確的結果,但是EnumMap作為枚舉的專屬的集合,我們沒有理由再去使用HashMap,畢竟EnumMap要求其Key必須為Enum類型,因而使用Color枚舉實例作為key是最恰當不過瞭,也避免瞭獲取name的步驟,更重要的是EnumMap效率更高,因為其內部是通過數組實現的(稍後分析),註意EnumMap的key值不能為null,雖說是枚舉專屬集合,但其操作與一般的Map差不多,概括性來說EnumMap是專門為枚舉類型量身定做的Map實現,雖然使用其它的Map(如HashMap)也能完成相同的功能,但是使用EnumMap會更加高效,它隻能接收同一枚舉類型的實例作為鍵值且不能為null,由於枚舉類型實例的數量相對固定並且有限,所以EnumMap使用數組來存放與枚舉類型對應的值,畢竟數組是一段連續的內存空間,根據程序局部性原理,效率會相當高。下面我們來進一步瞭解EnumMap的用法,先看構造函數:
//創建一個具有指定鍵類型的空枚舉映射。 EnumMap(Class<K> keyType) //創建一個其鍵類型與指定枚舉映射相同的枚舉映射,最初包含相同的映射關系(如果有的話)。 EnumMap(EnumMap<K,? extends V> m) //創建一個枚舉映射,從指定映射對其初始化。 EnumMap(Map<K,? extends V> m)
與HashMap不同,它需要傳遞一個類型信息,即Class對象,通過這個參數EnumMap就可以根據類型信息初始化其內部數據結構,另外兩隻是初始化時傳入一個Map集合,代碼演示如下:
//使用第一種構造 Map<Color,Integer> enumMap=new EnumMap<>(Color.class); //使用第二種構造 Map<Color,Integer> enumMap2=new EnumMap<>(enumMap); //使用第三種構造 Map<Color,Integer> hashMap = new HashMap<>(); hashMap.put(Color.GREEN, 2); hashMap.put(Color.BLUE, 3); Map<Color, Integer> enumMap = new EnumMap<>(hashMap);
至於EnumMap的方法,跟普通的map幾乎沒有區別,註意與HashMap的主要不同在於構造方法需要傳遞類型參數和EnumMap保證Key順序與枚舉中的順序一致,但請記住Key不能為null。
EnumMap實現原理剖析
EnumMap的源碼有700多行,這裡我們主要分析其內部存儲結構,添加查找的實現,瞭解這幾點,對應EnumMap內部實現原理也就比較清晰瞭,先看數據結構和構造函數
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { //Class對象引用 private final Class<K> keyType; //存儲Key值的數組 private transient K[] keyUniverse; //存儲Value值的數組 private transient Object[] vals; //map的size private transient int size = 0; //空map private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0]; //構造函數 public EnumMap(Class<K> keyType) { this.keyType = keyType; keyUniverse = getKeyUniverse(keyType); vals = new Object[keyUniverse.length]; } }
EnumMap繼承瞭AbstractMap類,因此EnumMap具備一般map的使用方法,keyType表示類型信息,keyUniverse表示鍵數組,存儲的是所有可能的枚舉值,vals數組表示鍵對應的值,size表示鍵值對個數。在構造函數中通過keyUniverse = getKeyUniverse(keyType);
初始化瞭keyUniverse數組的值,內部存儲的是所有可能的枚舉值,接著初始化瞭存在Value值得數組vals,其大小與枚舉實例的個數相同,getKeyUniverse方法實現如下
//返回枚舉數組 private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) { //最終調用到枚舉類型的values方法,values方法返回所有可能的枚舉值 return SharedSecrets.getJavaLangAccess() .getEnumConstantsShared(keyType); }
從方法的返回值來看,返回類型是枚舉數組,事實也是如此,最終返回值正是枚舉類型的values方法的返回值,前面我們分析過values方法返回所有可能的枚舉值,因此keyUniverse數組存儲就是枚舉類型的所有可能的枚舉值。接著看put方法的實現
public V put(K key, V value) { typeCheck(key);//檢測key的類型 //獲取存放value值得數組下標 int index = key.ordinal(); //獲取舊值 Object oldValue = vals[index]; //設置value值 vals[index] = maskNull(value); if (oldValue == null) size++; return unmaskNull(oldValue);//返回舊值 }
這裡通過typeCheck方法進行瞭key類型檢測,判斷是否為枚舉類型,如果類型不對,會拋出異常
private void typeCheck(K key) { Class<?> keyClass = key.getClass();//獲取類型信息 if (keyClass != keyType && keyClass.getSuperclass() != keyType) throw new ClassCastException(keyClass + " != " + keyType); }
接著通過int index = key.ordinal()
的方式獲取到該枚舉實例的順序值,利用此值作為下標,把值存儲在vals數組對應下標的元素中即vals[index]
,這也是為什麼EnumMap能維持與枚舉實例相同存儲順序的原因,我們發現在對vals[]中元素進行賦值和返回舊值時分別調用瞭maskNull方法和unmaskNull方法
//代表NULL值得空對象實例 private static final Object NULL = new Object() { public int hashCode() { return 0; } public String toString() { return "java.util.EnumMap.NULL"; } }; private Object maskNull(Object value) { //如果值為空,返回NULL對象,否則返回value return (value == null ? NULL : value); } @SuppressWarnings("unchecked") private V unmaskNull(Object value) { //將NULL對象轉換為null值 return (V)(value == NULL ? null : value); }
由此看來EnumMap還是允許存放null值的,但key絕對不能為null,對於null值,EnumMap進行瞭特殊處理,將其包裝為NULL對象,畢竟vals[]存的是Object,maskNull方法和unmaskNull方法正是用於null的包裝和解包裝的。這就是EnumMap集合的添加過程。下面接著看獲取方法
public V get(Object key) { return (isValidKey(key) ? unmaskNull(vals[((Enum<?>)key).ordinal()]) : null); } //對Key值的有效性和類型信息進行判斷 private boolean isValidKey(Object key) { if (key == null) return false; // Cheaper than instanceof Enum followed by getDeclaringClass Class<?> keyClass = key.getClass(); return keyClass == keyType || keyClass.getSuperclass() == keyType; }
話,直接通過ordinal方法取索引,然後在值數組vals裡通過索引獲取值返回。remove方法如下:
public V remove(Object key) { //判斷key值是否有效 if (!isValidKey(key)) return null; //直接獲取索引 int index = ((Enum<?>)key).ordinal(); Object oldValue = vals[index]; //對應下標元素值設置為null vals[index] = null; if (oldValue != null) size--;//減size return unmaskNull(oldValue); }
非常簡單,key值有效,通過key獲取下標索引值,把vals[]對應下標值設置為null,size減一。查看是否包含某個值,
判斷是否包含某value public boolean containsValue(Object value) { value = maskNull(value); //遍歷數組實現 for (Object val : vals) if (value.equals(val)) return true; return false; } //判斷是否包含key public boolean containsKey(Object key) { return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null; }
判斷value直接通過遍歷數組實現,而判斷key就更簡單瞭,判斷key是否有效和對應vals[]中是否存在該值。ok~,這就是EnumMap的主要實現原理,即內部有兩個數組,長度相同,一個表示所有可能的鍵(枚舉值),一個表示對應的值,不允許keynull,但允許value為null,鍵都有一個對應的索引,根據索引直接訪問和操作其鍵數組和值數組,由於操作都是數組,因此效率很高。
EnumSet
EnumSet是與枚舉類型一起使用的專用 Set 集合,EnumSet 中所有元素都必須是枚舉類型。與其他Set接口的實現類HashSet/TreeSet(內部都是用對應的HashMap/TreeMap實現的)不同的是,EnumSet在內部實現是位向量(稍後分析),它是一種極為高效的位運算操作,由於直接存儲和操作都是bit,因此EnumSet空間和時間性能都十分可觀,足以媲美傳統上基於 int 的“位標志”的運算,重要的是我們可像操作set集合一般來操作位運算,這樣使用代碼更簡單易懂同時又具備類型安全的優勢。註意EnumSet不允許使用 null 元素。試圖插入 null 元素將拋出 NullPointerException,但試圖測試判斷是否存在null 元素或移除 null 元素則不會拋出異常,與大多數collection 實現一樣,EnumSet不是線程安全的,因此在多線程環境下應該註意數據同步問題,ok~,下面先來簡單看看EnumSet的使用方式。
EnumSet用法
創建EnumSet並不能使用new關鍵字,因為它是個抽象類,而應該使用其提供的靜態工廠方法,EnumSet的靜態工廠方法比較多,如下:
創建一個具有指定元素類型的空EnumSet。 EnumSet<E> noneOf(Class<E> elementType) //創建一個指定元素類型並包含所有枚舉值的EnumSet <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) // 創建一個包括枚舉值中指定范圍元素的EnumSet <E extends Enum<E>> EnumSet<E> range(E from, E to) // 初始集合包括指定集合的補集 <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s) // 創建一個包括參數中所有元素的EnumSet <E extends Enum<E>> EnumSet<E> of(E e) <E extends Enum<E>> EnumSet<E> of(E e1, E e2) <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3) <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4) <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5) <E extends Enum<E>> EnumSet<E> of(E first, E... rest) //創建一個包含參數容器中的所有元素的EnumSet <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s) <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)
代碼演示如下:
import java.util.ArrayList; import java.util.EnumSet; import java.util.List; /** * Created by wuzejian on 2017/5/12. * */ enum Color { GREEN , RED , BLUE , BLACK , YELLOW } public class EnumSetDemo { public static void main(String[] args){ //空集合 EnumSet<Color> enumSet= EnumSet.noneOf(Color.class); System.out.println("添加前:"+enumSet.toString()); enumSet.add(Color.GREEN); enumSet.add(Color.RED); enumSet.add(Color.BLACK); enumSet.add(Color.BLUE); enumSet.add(Color.YELLOW); System.out.println("添加後:"+enumSet.toString()); System.out.println("-----------------------------------"); //使用allOf創建包含所有枚舉類型的enumSet,其內部根據Class對象初始化瞭所有枚舉實例 EnumSet<Color> enumSet1= EnumSet.allOf(Color.class); System.out.println("allOf直接填充:"+enumSet1.toString()); System.out.println("-----------------------------------"); //初始集合包括枚舉值中指定范圍的元素 EnumSet<Color> enumSet2= EnumSet.range(Color.BLACK,Color.YELLOW); System.out.println("指定初始化范圍:"+enumSet2.toString()); System.out.println("-----------------------------------"); //指定補集,也就是從全部枚舉類型中去除參數集合中的元素,如下去掉上述enumSet2的元素 EnumSet<Color> enumSet3= EnumSet.complementOf(enumSet2); System.out.println("指定補集:"+enumSet3.toString()); System.out.println("-----------------------------------"); //初始化時直接指定元素 EnumSet<Color> enumSet4= EnumSet.of(Color.BLACK); System.out.println("指定Color.BLACK元素:"+enumSet4.toString()); EnumSet<Color> enumSet5= EnumSet.of(Color.BLACK,Color.GREEN); System.out.println("指定Color.BLACK和Color.GREEN元素:"+enumSet5.toString()); System.out.println("-----------------------------------"); //復制enumSet5容器的數據作為初始化數據 EnumSet<Color> enumSet6= EnumSet.copyOf(enumSet5); System.out.println("enumSet6:"+enumSet6.toString()); System.out.println("-----------------------------------"); List<Color> list = new ArrayList<Color>(); list.add(Color.BLACK); list.add(Color.BLACK);//重復元素 list.add(Color.RED); list.add(Color.BLUE); System.out.println("list:"+list.toString()); //使用copyOf(Collection<E> c) EnumSet enumSet7=EnumSet.copyOf(list); System.out.println("enumSet7:"+enumSet7.toString()); /** 輸出結果: 添加前:[] 添加後:[GREEN, RED, BLUE, BLACK, YELLOW] ----------------------------------- allOf直接填充:[GREEN, RED, BLUE, BLACK, YELLOW] ----------------------------------- 指定初始化范圍:[BLACK, YELLOW] ----------------------------------- 指定補集:[GREEN, RED, BLUE] ----------------------------------- 指定Color.BLACK元素:[BLACK] 指定Color.BLACK和Color.GREEN元素:[GREEN, BLACK] ----------------------------------- enumSet6:[GREEN, BLACK] ----------------------------------- list:[BLACK, BLACK, RED, BLUE] enumSet7:[RED, BLUE, BLACK] */ } }
到此這篇關於秒懂Java枚舉類型(enum)的文章就介紹到這瞭,更多相關Java枚舉類型內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 一文秒懂Java enum常見的用法講解
- Java 枚舉類和自定義枚舉類和enum聲明及實現接口的操作
- 深入淺出講解Java中的枚舉類
- Java實例講解枚舉enum的實現
- Java基礎之枚舉Enum類案例詳解