您好,登錄后才能下訂單哦!
Java中枚舉類型的作用是什么?針對這個問題,這篇文章詳細(xì)介紹了相對應(yīng)的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
枚舉類型是Java 5中新增特性的一部分,它是一種特殊的數(shù)據(jù)類型,之所以特殊是因為它既是一種類(class)類型卻又比類類型多了些特殊的約束,但是這些約束的存在也造就了枚舉類型的簡潔性、安全性以及便捷性。下面先來看看什么是枚舉?如何定義枚舉?
上述的常量定義常量的方式稱為int枚舉模式,這樣的定義方式并沒有什么錯,但它存在許多不足,如在類型安全和使用方便性上并沒有多少好處,如果存在定義int值相同的變量,混淆的幾率還是很大的,編譯器也不會提出任何警告,因此這種方式在枚舉出現(xiàn)后并不提倡,現(xiàn)在我們利用枚舉類型來重新定義上述的常量,同時也感受一把枚舉定義的方式,如下定義周一到周日的常量
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; }
相當(dāng)簡潔,在定義枚舉類型時我們使用的關(guān)鍵字是enum,與class關(guān)鍵字類似,只不過前者是定義枚舉類型,后者是定義類類型。枚舉類型Day中分別定義了從周一到周日的值,這里要注意,值一般是大寫的字母,多個值之間以逗號分隔。同時我們應(yīng)該知道的是枚舉類型可以像類(class)類型一樣,定義為一個單獨的文件,當(dāng)然也可以定義在其他類內(nèi)部,更重要的是枚舉常量在類型安全性和便捷性都很有保證,如果出現(xiàn)類型問題編譯器也會提示我們改進(jìn),但務(wù)必記住枚舉表示的類型其取值是必須有限的,也就是說每個值都是可以枚舉出來的,比如上述描述的一周共有七天。那么該如何使用呢?如下:
//枚舉類型,使用關(guān)鍵字enum enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
就像上述代碼那樣,直接引用枚舉的值即可,這便是枚舉類型的最簡單模型。
我們大概了解了枚舉類型的定義與簡單使用后,現(xiàn)在有必要來了解一下枚舉類型的基本實現(xiàn)原理。實際上在使用關(guān)鍵字enum創(chuàng)建枚舉類型并編譯后,編譯器會為我們生成一個相關(guān)的類,這個類繼承了Java API中的java.lang.Enum類,也就是說通過關(guān)鍵字enum創(chuàng)建枚舉類型在編譯后事實上也是一個類類型而且該類繼承自java.lang.Enum類。下面我們編譯前面定義的EnumDemo.java并查看生成的class文件來驗證這個結(jié)論:
javac EnumDemo.java Day.class EnumDemo.class EnumDemo.java
利用javac編譯前面定義的EnumDemo.java文件后分別生成了Day.class和EnumDemo.class文件,而Day.class就是枚舉類型,這也就驗證前面所說的使用關(guān)鍵字enum定義枚舉類型并編譯后,編譯器會自動幫助我們生成一個與枚舉相關(guān)的類。我們再來看看反編譯Day.class文件:
//反編譯Day.class final class Day extends Enum { //編譯器為我們添加的靜態(tài)的values()方法 public static Day[] values() { return (Day[])$VALUES.clone(); } //編譯器為我們添加的靜態(tài)的valueOf()方法,注意間接調(diào)用了Enum也類的valueOf方法 public static Day valueOf(String s) { return (Day)Enum.valueOf(com/zejian/enumdemo/Day, s); } //私有構(gòu)造函數(shù) 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類型的實例對象分別對應(yīng)枚舉中定義的7個日期,這也充分說明了我們前面使用關(guān)鍵字enum定義的Day類型中的每種日期枚舉常量也是實實在在的Day實例對象,只不過代表的內(nèi)容不一樣而已。注意編譯器還為我們生成了兩個靜態(tài)方法,分別是values()和 valueOf(),稍后會分析它們的用法,到此我們也就明白了,使用關(guān)鍵字enum定義的枚舉類型,在編譯期后,也將轉(zhuǎn)換成為一個實實在在的類,而在該類中,會存在每個在枚舉類型中定義好變量的對應(yīng)實例對象,如上述的MONDAY枚舉類型對應(yīng)public static final Day MONDAY;
,同時編譯器會為該類創(chuàng)建兩個方法,分別是values()和valueOf()。ok~,到此相信我們對枚舉的實現(xiàn)原理也比較清晰,下面我們深入了解一下java.lang.Enum類以及values()和valueOf()的用途。
Enum抽象類常用方法
Enum是所有 Java 語言枚舉類型的公共基本類(注意Enum是抽象類),以下是它的常見方法:
返回類型 | 方法名稱 | 方法說明 |
---|---|---|
int | compareTo(E o) | 比較此枚舉與指定對象的順序 |
boolean | equals(Object other) | 當(dāng)指定對象等于此枚舉常量時,返回 true。 |
Class<?> | getDeclaringClass() | 返回與此枚舉常量的枚舉類型相對應(yīng)的 Class 對象 |
String | name() | 返回此枚舉常量的名稱,在其枚舉聲明中對其進(jìn)行聲明 |
int | ordinal() | 返回枚舉常量的序數(shù)(它在枚舉聲明中的位置,其中初始常量序數(shù)為零) |
String | toString() | 返回枚舉常量的名稱,它包含在聲明中 |
static<T extends Enum<T>> T | static valueOf(Class<T> enumType, String name) | 返回帶指定名稱的指定枚舉類型的枚舉常量。 |
這里主要說明一下ordinal()
方法,該方法獲取的是枚舉變量在枚舉類中聲明的順序,下標(biāo)從0開始,如日期中的MONDAY在第一個位置,那么MONDAY的ordinal值就是0,如果MONDAY的聲明位置發(fā)生變化,那么ordinal方法獲取到的值也隨之變化,注意在大多數(shù)情況下我們都不應(yīng)該首先使用該方法,畢竟它總是變幻莫測的。compareTo(E o)
方法則是比較枚舉的大小,注意其內(nèi)部實現(xiàn)是根據(jù)每個枚舉的ordinal值大小進(jìn)行比較的。name()
方法與toString()
幾乎是等同的,都是輸出變量的字符串形式。至于valueOf(Class<T> enumType, String name)
方法則是根據(jù)枚舉類的Class對象和枚舉名稱獲取枚舉常量,注意該方法是靜態(tài)的,后面在枚舉單例時,我們還會詳細(xì)分析該方法,下面的代碼演示了上述方法:
public class EnumDemo { public static void main(String[] args){ //創(chuàng)建枚舉數(shù)組 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方法比較,實際上其內(nèi)部是通過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對象引用,當(dāng)然也可以通過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); } /** 執(zhí)行結(jié)果: 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類的基本內(nèi)容就介紹完了,這里提醒大家一點,Enum類內(nèi)部會有一個構(gòu)造函數(shù),該構(gòu)造函數(shù)只能有編譯器調(diào)用,我們是無法手動操作的,不妨看看Enum類的主要源碼:
//實現(xiàn)了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; } //枚舉的構(gòu)造方法,只能由編譯器調(diào)用 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;//根據(jù)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對象內(nèi)部的方法,根據(jù)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實現(xiàn)了Comparable接口,這也是可以使用compareTo比較的原因,當(dāng)然Enum構(gòu)造函數(shù)也是存在的,該函數(shù)只能由編譯器調(diào)用,畢竟我們只能使用enum關(guān)鍵字定義枚舉,其他事情就放心交給編譯器吧。
//由編譯器調(diào)用 protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }
編譯器生成的Values方法與ValueOf方法
values()方法和valueOf(String name)方法是編譯器生成的static方法,因此從前面的分析中,在Enum類中并沒出現(xiàn)values()方法,但valueOf()方法還是有出現(xiàn)的,只不過編譯器生成的valueOf()方法需傳遞一個name參數(shù),而Enum自帶的靜態(tài)方法valueOf()則需要傳遞兩個方法,從前面反編譯后的代碼可以看出,編譯器生成的valueOf方法最終還是調(diào)用了Enum類的valueOf方法,下面通過代碼來演示這兩個方法的作用:
Day[] days2 = Day.values(); System.out.println("day2:"+Arrays.toString(days2)); Day day = Day.valueOf("MONDAY"); System.out.println("day:"+day); /** 輸出結(jié)果: day2:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY] day:MONDAY */
從結(jié)果可知道,values()方法的作用就是獲取枚舉類中的所有變量,并作為數(shù)組返回,而valueOf(String name)方法與Enum類中的valueOf方法的作用類似根據(jù)名稱獲取枚舉變量,只不過編譯器生成的valueOf方法更簡潔些只需傳遞一個參數(shù)。這里我們還必須注意到,由于values()方法是由編譯器插入到枚舉類中的static方法,所以如果我們將枚舉實例向上轉(zhuǎn)型為Enum,那么values()方法將無法被調(diào)用,因為Enum類中并沒有values()方法,valueOf()方法也是同樣的道理,注意是一個參數(shù)的。
//正常使用 Day[] ds=Day.values(); //向上轉(zhuǎn)型Enum Enum e = Day.MONDAY; //無法調(diào)用,沒有此方法 //e.values();
上述我們提到當(dāng)枚舉實例向上轉(zhuǎn)型為Enum類型后,values()方法將會失效,也就無法一次性獲取所有枚舉實例變量,但是由于Class對象的存在,即使不使用values()方法,還是有可能一次獲取到所有枚舉實例變量的,在Class對象中存在如下方法:
返回類型 | 方法名稱 | 方法說明 |
---|---|---|
T[] | getEnumConstants() | 返回該枚舉類型的所有元素,如果Class對象不是枚舉類型,則返回null。 |
boolean | isEnum() | 當(dāng)且僅當(dāng)該類聲明為源代碼中的枚舉時返回 true |
因此通過getEnumConstants()方法,同樣可以輕而易舉地獲取所有枚舉實例變量下面通過代碼來演示這個功能:
//正常使用 Day[] ds=Day.values(); //向上轉(zhuǎn)型Enum Enum e = Day.MONDAY; //無法調(diào)用,沒有此方法 //e.values(); //獲取class對象引用 Class<?> clasz = e.getDeclaringClass(); if(clasz.isEnum()) { Day[] dsz = (Day[]) clasz.getEnumConstants(); System.out.println("dsz:"+Arrays.toString(dsz)); } /** 輸出結(jié)果: dsz:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY] */
正如上述代碼所展示,通過Enum的class對象的getEnumConstants方法,我們?nèi)阅芤淮涡垣@取所有的枚舉實例常量。
在前面的分析中,我們都是基于簡單枚舉類型的定義,也就是在定義枚舉時只定義了枚舉實例類型,并沒定義方法或者成員變量,實際上使用關(guān)鍵字enum定義的枚舉類,除了不能使用繼承(因為編譯器會自動為我們繼承Enum抽象類而Java只支持單繼承,因此枚舉類是無法手動實現(xiàn)繼承的),可以把enum類當(dāng)成常規(guī)類,也就是說我們可以向enum類中添加方法和變量,甚至是mian方法,下面就來感受一把。
向enum類添加方法與自定義構(gòu)造函數(shù)
重新定義一個日期枚舉類,帶有desc成員變量描述該日期的對于中文描述,同時定義一個getDesc方法,返回中文描述內(nèi)容,自定義私有構(gòu)造函數(shù),在聲明枚舉實例時傳入對應(yīng)的中文描述,代碼如下:
public enum Day2 { MONDAY("星期一"), TUESDAY("星期二"), WEDNESDAY("星期三"), THURSDAY("星期四"), FRIDAY("星期五"), SATURDAY("星期六"), SUNDAY("星期日");//記住要用分號結(jié)束 private String desc;//中文描述 /** * 私有構(gòu)造,防止被外部調(diào)用 * @param desc */ private Day2(String desc){ this.desc=desc; } /** * 定義方法,返回描述,跟常規(guī)類的定義沒區(qū)別 * @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()); } } /** 輸出結(jié)果: name:MONDAY,desc:星期一 name:TUESDAY,desc:星期二 name:WEDNESDAY,desc:星期三 name:THURSDAY,desc:星期四 name:FRIDAY,desc:星期五 name:SATURDAY,desc:星期六 name:SUNDAY,desc:星期日 */ }
從上述代碼可知,在enum類中確實可以像定義常規(guī)類一樣聲明變量或者成員方法。但是我們必須注意到,如果打算在enum類中定義方法,務(wù)必在聲明完枚舉實例后使用分號分開,倘若在枚舉實例前定義任何方法,編譯器都將會報錯,無法編譯通過,同時即使自定義了構(gòu)造函數(shù)且enum的定義結(jié)束,我們也永遠(yuǎn)無法手動調(diào)用構(gòu)造函數(shù)創(chuàng)建枚舉實例,畢竟這事只能由編譯器執(zhí)行。
既然enum類跟常規(guī)類的定義沒什么區(qū)別(實際上enum還是有些約束的),那么覆蓋父類的方法也不會是什么難說,可惜的是父類Enum中的定義的方法只有toString方法沒有使用final修飾,因此只能覆蓋toString方法,如下通過覆蓋toString省去了getDesc方法:
public enum Day2 { MONDAY("星期一"), TUESDAY("星期二"), WEDNESDAY("星期三"), THURSDAY("星期四"), FRIDAY("星期五"), SATURDAY("星期六"), SUNDAY("星期日");//記住要用分號結(jié)束 private String desc;//中文描述 /** * 私有構(gòu)造,防止被外部調(diào)用 * @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()); } } /** 輸出結(jié)果: name:MONDAY,desc:星期一 name:TUESDAY,desc:星期二 name:WEDNESDAY,desc:星期三 name:THURSDAY,desc:星期四 name:FRIDAY,desc:星期五 name:SATURDAY,desc:星期六 name:SUNDAY,desc:星期日 */ }
enum類中定義抽象方法
與常規(guī)抽象類一樣,enum類允許我們?yōu)槠涠x抽象方法,然后使每個枚舉實例都實現(xiàn)該方法,以便產(chǎn)生不同的行為方式,注意abstract關(guān)鍵字對于枚舉類來說并不是必須的如下:
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()); /** 輸出結(jié)果: F:FIRST TIME S:SECOND TIME */ } }
通過這種方式就可以輕而易舉地定義每個枚舉實例的不同行為方式。我們可能注意到,enum類的實例似乎表現(xiàn)出了多態(tài)的特性,可惜的是枚舉類型的實例終究不能作為類型傳遞使用,就像下面的使用方式,編譯器是不可能答應(yīng)的:
//無法通過編譯,畢竟EnumDemo3.FIRST是個實例對象 public void text(EnumDemo3.FIRST instance){ }
在枚舉實例常量中定義抽象方法
由于Java單繼承的原因,enum類并不能再繼承其它類,但并不妨礙它實現(xiàn)接口,因此enum類同樣是可以實現(xiàn)多接口的,如下:
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....."); } }
有時候,我們可能需要對一組數(shù)據(jù)進(jìn)行分類,比如進(jìn)行食物菜單分類而且希望這些菜單都屬于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,現(xiàn)在我們利用一個枚舉嵌套枚舉的方式,把前面定義的菜譜存放到一個Meal菜單中,通過這種方式就可以統(tǒng)一管理菜單的數(shù)據(jù)了。
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; } } }
關(guān)于枚舉與switch是個比較簡單的話題,使用switch進(jìn)行條件判斷時,條件參數(shù)一般只能是整型,字符型。而枚舉型確實也被switch所支持,在java 1.7后switch也對字符串進(jìn)行了支持。這里我們簡單看一下switch與枚舉類型的使用:
enum Color {GREEN,RED,BLUE} public class EnumDemo4 { public static void printName(Color color){ switch (color){ case BLUE: //無需使用Color進(jìn)行引用 System.out.println("藍(lán)色"); 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); //藍(lán)色 //紅色 //綠色 } }
需要注意的是使用在于switch條件進(jìn)行結(jié)合使用時,無需使用Color引用。
單例模式可以說是最常使用的設(shè)計模式了,它的作用是確保某個類只有一個實例,自行實例化并向整個系統(tǒng)提供這個實例。在實際應(yīng)用中,線程池、緩存、日志對象、對話框?qū)ο蟪1辉O(shè)計成單例,總之,選擇單例模式就是為了避免不一致狀態(tài),下面我們將會簡單說明單例模式的幾種主要編寫方式,從而對比出使用枚舉實現(xiàn)單例模式的優(yōu)點。首先看看餓漢式的單例模式:
/** * 餓漢式(基于classloder機(jī)制避免了多線程的同步問題) */ public class SingletonHungry { private static SingletonHungry instance = new SingletonHungry(); private SingletonHungry() { } public static SingletonHungry getInstance() { return instance; } }
顯然這種寫法比較簡單,但問題是無法做到延遲創(chuàng)建對象,事實上如果該單例類涉及資源較多,創(chuàng)建比較耗時間時,我們更希望它可以盡可能地延遲加載,從而減小初始化的負(fù)載,于是便有了如下的懶漢式單例:
/** * 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機(jī)制,遺憾的是,由于synchronized的存在,效率很低,在單線程的情景下,完全可以去掉synchronized,為了兼顧效率與性能問題,改進(jìn)后代碼如下:
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()方法中,進(jìn)行兩次null檢查。這樣可以極大提升并發(fā)度,進(jìn)而提升性能。畢竟在單例中new的情況非常少,絕大多數(shù)都是可以并行的讀操作,因此在加鎖前多進(jìn)行一次null檢查就可以減少絕大多數(shù)的加鎖操作,也就提高了執(zhí)行效率。但是必須注意的是volatile關(guān)鍵字,該關(guān)鍵字有兩層語義。第一層語義是可見性,可見性是指在一個線程中對該變量的修改會馬上由工作內(nèi)存(Work Memory)寫回主內(nèi)存(Main Memory),所以其它線程會馬上讀取到已修改的值,關(guān)于工作內(nèi)存和主內(nèi)存可簡單理解為高速緩存(直接與CPU打交道)和主存(日常所說的內(nèi)存條),注意工作內(nèi)存是線程獨享的,主存是線程共享的。volatile的第二層語義是禁止指令重排序優(yōu)化,我們寫的代碼(特別是多線程代碼),由于編譯器優(yōu)化,在實際執(zhí)行的時候可能與我們編寫的順序不同。編譯器只保證程序執(zhí)行結(jié)果與源代碼相同,卻不保證實際指令的順序與源代碼相同,這在單線程并沒什么問題,然而一旦引入多線程環(huán)境,這種亂序就可能導(dǎo)致嚴(yán)重問題。volatile關(guān)鍵字就可以從語義上解決這個問題,值得關(guān)注的是volatile的禁止指令重排序優(yōu)化功能在Java 1.5后才得以實現(xiàn),因此1.5前的版本仍然是不安全的,即使使用了volatile關(guān)鍵字?;蛟S我們可以利用靜態(tài)內(nèi)部類來實現(xiàn)更安全的機(jī)制,靜態(tài)內(nèi)部類單例模式如下:
/** * Created by wuzejian on 2017/5/9. * 靜態(tài)內(nèi)部類 */ public class SingletonInner { private static class Holder { private static SingletonInner singleton = new SingletonInner(); } private SingletonInner(){} public static SingletonInner getSingleton(){ return Holder.singleton; } }
正如上述代碼所展示的,我們把Singleton實例放到一個靜態(tài)內(nèi)部類中,這樣可以避免了靜態(tài)實例在Singleton類的加載階段(類加載過程的其中一個階段的,此時只創(chuàng)建了Class對象,關(guān)于Class對象可以看博主另外一篇博文,深入理解Java類型信息(Class對象)與反射機(jī)制)就創(chuàng)建對象,畢竟靜態(tài)變量初始化是在SingletonInner類初始化時觸發(fā)的,并且由于靜態(tài)內(nèi)部類只會被加載一次,所以這種寫法也是線程安全的。從上述4種單例模式的寫法中,似乎也解決了效率與懶加載的問題,但是它們都有兩個共同的缺點:
序列化可能會破壞單例模式,比較每次反序列化一個序列化的對象實例時都會創(chuàng)建一個新的實例,解決方案如下:
//測試?yán)?四種寫解決方式雷同) public class Singleton implements java.io.Serializable { public static Singleton INSTANCE = new Singleton(); protected Singleton() { } //反序列時直接返回當(dāng)前INSTANCE private Object readResolve() { return INSTANCE; } }
使用反射強(qiáng)行調(diào)用私有構(gòu)造器,解決方式可以修改構(gòu)造器,讓它在創(chuàng)建第二個實例的時候拋異常,如下:
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 !"); } }
如上所述,問題確實也得到了解決,但問題是我們?yōu)榇烁冻隽瞬簧倥?,即添加了不少代碼,還應(yīng)該注意到如果單例類維持了其他對象的狀態(tài)時還需要使他們成為transient的對象,這種就更復(fù)雜了,那有沒有更簡單更高效的呢?當(dāng)然是有的,那就是枚舉單例了,先來看看如何實現(xiàn):
/** * 枚舉單利 */ public enum SingletonEnum { INSTANCE; private String name; public String getName(){ return name; } public void setName(String name){ this.name = name; } }
代碼相當(dāng)簡潔,我們也可以像常規(guī)類一樣編寫enum類,為其添加變量和方法,訪問方式也更簡單,使用SingletonEnum.INSTANCE
進(jìn)行訪問,這樣也就避免調(diào)用getInstance方法,更重要的是使用枚舉單例的寫法,我們完全不用考慮序列化和反射的問題。枚舉序列化是由jvm保證的,每一個枚舉類型和定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java做了特殊的規(guī)定:在序列化時Java僅僅是將枚舉對象的name屬性輸出到結(jié)果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據(jù)名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機(jī)制的定制的并禁用了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); }
實際上通過調(diào)用enumType(Class對象的引用)的enumConstantDirectory方法獲取到的是一個Map集合,在該集合中存放了以枚舉name為key和以枚舉實例變量為value的Key&Value數(shù)據(jù),因此通過name的值就可以獲取到枚舉實例,看看enumConstantDirectory方法源碼:
Map<String, T> enumConstantDirectory() { if (enumConstantDirectory == null) { //getEnumConstantsShared最終通過反射調(diào)用枚舉類的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存放了當(dāng)前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;
到這里我們也就可以看出枚舉序列化確實不會重新創(chuàng)建新實例,jvm保證了每個枚舉實例變量的唯一性。再來看看反射到底能不能創(chuàng)建枚舉,下面試圖通過反射獲取構(gòu)造器并創(chuàng)建枚舉
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { //獲取枚舉類的構(gòu)造函數(shù)(前面的源碼已分析過) Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class); constructor.setAccessible(true); //創(chuàng)建枚舉 SingletonEnum singleton=constructor.newInstance("otherInstance",9); }
執(zhí)行報錯
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)
顯然告訴我們不能使用反射創(chuàng)建枚舉類,這是為什么呢?不妨看看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; }
源碼很了然,確實無法使用反射創(chuàng)建枚舉實例,也就是說明了創(chuàng)建枚舉實例只有編譯器能夠做到而已。顯然枚舉單例模式確實是很不錯的選擇,因此我們推薦使用它。但是這總不是萬能的,對于android平臺這個可能未必是最好的選擇,在android開發(fā)中,內(nèi)存優(yōu)化是個大塊頭,而使用枚舉時占用的內(nèi)存常常是靜態(tài)變量的兩倍還多,因此android官方在內(nèi)存優(yōu)化方面給出的建議是盡量避免在android中使用enum。但是不管如何,關(guān)于單例,我們總是應(yīng)該記?。壕€程安全,延遲加載,序列化與反序列化安全,反射安全是很重重要的。
先思考這樣一個問題,現(xiàn)在我們有一堆size大小相同而顏色不同的數(shù)據(jù),需要統(tǒng)計出每種顏色的數(shù)量是多少以便將數(shù)據(jù)錄入倉庫,定義如下枚舉用于表示顏色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()); } /** 輸出結(jié)果: {RED=2, BLUE=3, YELLOW=3, GREEN=2} --------------- {GREEN=2, RED=2, BLUE=3, YELLOW=3} */ }
代碼比較簡單,我們使用兩種解決方案,一種是HashMap,一種EnumMap,雖然都統(tǒng)計出了正確的結(jié)果,但是EnumMap作為枚舉的專屬的集合,我們沒有理由再去使用HashMap,畢竟EnumMap要求其Key必須為Enum類型,因而使用Color枚舉實例作為key是最恰當(dāng)不過了,也避免了獲取name的步驟,更重要的是EnumMap效率更高,因為其內(nèi)部是通過數(shù)組實現(xiàn)的(稍后分析),注意EnumMap的key值不能為null,雖說是枚舉專屬集合,但其操作與一般的Map差不多,概括性來說EnumMap是專門為枚舉類型量身定做的Map實現(xiàn),雖然使用其它的Map(如HashMap)也能完成相同的功能,但是使用EnumMap會更加高效,它只能接收同一枚舉類型的實例作為鍵值且不能為null,由于枚舉類型實例的數(shù)量相對固定并且有限,所以EnumMap使用數(shù)組來存放與枚舉類型對應(yīng)的值,畢竟數(shù)組是一段連續(xù)的內(nèi)存空間,根據(jù)程序局部性原理,效率會相當(dāng)高。下面我們來進(jìn)一步了解EnumMap的用法,先看構(gòu)造函數(shù):
//創(chuàng)建一個具有指定鍵類型的空枚舉映射。 EnumMap(Class<K> keyType) //創(chuàng)建一個其鍵類型與指定枚舉映射相同的枚舉映射,最初包含相同的映射關(guān)系(如果有的話)。 EnumMap(EnumMap<K,? extends V> m) //創(chuàng)建一個枚舉映射,從指定映射對其初始化。 EnumMap(Map<K,? extends V> m)
與HashMap不同,它需要傳遞一個類型信息,即Class對象,通過這個參數(shù)EnumMap就可以根據(jù)類型信息初始化其內(nèi)部數(shù)據(jù)結(jié)構(gòu),另外兩只是初始化時傳入一個Map集合,代碼演示如下:
//使用第一種構(gòu)造 Map<Color,Integer> enumMap=new EnumMap<>(Color.class); //使用第二種構(gòu)造 Map<Color,Integer> enumMap2=new EnumMap<>(enumMap); //使用第三種構(gòu)造 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幾乎沒有區(qū)別,注意與HashMap的主要不同在于構(gòu)造方法需要傳遞類型參數(shù)和EnumMap保證Key順序與枚舉中的順序一致,但請記住Key不能為null。
EnumMap的源碼有700多行,這里我們主要分析其內(nèi)部存儲結(jié)構(gòu),添加查找的實現(xiàn),了解這幾點,對應(yīng)EnumMap內(nèi)部實現(xiàn)原理也就比較清晰了,先看數(shù)據(jù)結(jié)構(gòu)和構(gòu)造函數(shù)
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { //Class對象引用 private final Class<K> keyType; //存儲Key值的數(shù)組 private transient K[] keyUniverse; //存儲Value值的數(shù)組 private transient Object[] vals; //map的size private transient int size = 0; //空map private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0]; //構(gòu)造函數(shù) public EnumMap(Class<K> keyType) { this.keyType = keyType; keyUniverse = getKeyUniverse(keyType); vals = new Object[keyUniverse.length]; } }
EnumMap繼承了AbstractMap類,因此EnumMap具備一般map的使用方法,keyType表示類型信息,keyUniverse表示鍵數(shù)組,存儲的是所有可能的枚舉值,vals數(shù)組表示鍵對應(yīng)的值,size表示鍵值對個數(shù)。在構(gòu)造函數(shù)中通過keyUniverse = getKeyUniverse(keyType);
初始化了keyUniverse數(shù)組的值,內(nèi)部存儲的是所有可能的枚舉值,接著初始化了存在Value值得數(shù)組vals,其大小與枚舉實例的個數(shù)相同,getKeyUniverse方法實現(xiàn)如下
//返回枚舉數(shù)組 private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) { //最終調(diào)用到枚舉類型的values方法,values方法返回所有可能的枚舉值 return SharedSecrets.getJavaLangAccess() .getEnumConstantsShared(keyType); }
從方法的返回值來看,返回類型是枚舉數(shù)組,事實也是如此,最終返回值正是枚舉類型的values方法的返回值,前面我們分析過values方法返回所有可能的枚舉值,因此keyUniverse數(shù)組存儲就是枚舉類型的所有可能的枚舉值。接著看put方法的實現(xiàn)
public V put(K key, V value) { typeCheck(key);//檢測key的類型 //獲取存放value值得數(shù)組下標(biāo) int index = key.ordinal(); //獲取舊值 Object oldValue = vals[index]; //設(shè)置value值 vals[index] = maskNull(value); if (oldValue == null) size++; return unmaskNull(oldValue);//返回舊值 }
這里通過typeCheck方法進(jìn)行了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()
的方式獲取到該枚舉實例的順序值,利用此值作為下標(biāo),把值存儲在vals數(shù)組對應(yīng)下標(biāo)的元素中即vals[index]
,這也是為什么EnumMap能維持與枚舉實例相同存儲順序的原因,我們發(fā)現(xiàn)在對vals[]中元素進(jìn)行賦值和返回舊值時分別調(diào)用了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對象轉(zhuǎn)換為null值 return (V)(value == NULL ? null : value); }
由此看來EnumMap還是允許存放null值的,但key絕對不能為null,對于null值,EnumMap進(jìn)行了特殊處理,將其包裝為NULL對象,畢竟vals[]存的是Object,maskNull方法和unmaskNull方法正是用于null的包裝和解包裝的。這就是EnumMap集合的添加過程。下面接著看獲取方法
public V get(Object key) { return (isValidKey(key) ? unmaskNull(vals[((Enum<?>)key).ordinal()]) : null); } //對Key值的有效性和類型信息進(jìn)行判斷 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方法取索引,然后在值數(shù)組vals里通過索引獲取值返回。remove方法如下:
public V remove(Object key) { //判斷key值是否有效 if (!isValidKey(key)) return null; //直接獲取索引 int index = ((Enum<?>)key).ordinal(); Object oldValue = vals[index]; //對應(yīng)下標(biāo)元素值設(shè)置為null vals[index] = null; if (oldValue != null) size--;//減size return unmaskNull(oldValue); }
非常簡單,key值有效,通過key獲取下標(biāo)索引值,把vals[]對應(yīng)下標(biāo)值設(shè)置為null,size減一。查看是否包含某個值,
判斷是否包含某value public boolean containsValue(Object value) { value = maskNull(value); //遍歷數(shù)組實現(xiàn) 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直接通過遍歷數(shù)組實現(xiàn),而判斷key就更簡單了,判斷key是否有效和對應(yīng)vals[]中是否存在該值。ok~,這就是EnumMap的主要實現(xiàn)原理,即內(nèi)部有兩個數(shù)組,長度相同,一個表示所有可能的鍵(枚舉值),一個表示對應(yīng)的值,不允許keynull,但允許value為null,鍵都有一個對應(yīng)的索引,根據(jù)索引直接訪問和操作其鍵數(shù)組和值數(shù)組,由于操作都是數(shù)組,因此效率很高。
EnumSet是與枚舉類型一起使用的專用 Set 集合,EnumSet 中所有元素都必須是枚舉類型。與其他Set接口的實現(xiàn)類HashSet/TreeSet(內(nèi)部都是用對應(yīng)的HashMap/TreeMap實現(xiàn)的)不同的是,EnumSet在內(nèi)部實現(xiàn)是位向量(稍后分析),它是一種極為高效的位運算操作,由于直接存儲和操作都是bit,因此EnumSet空間和時間性能都十分可觀,足以媲美傳統(tǒng)上基于 int 的“位標(biāo)志”的運算,重要的是我們可像操作set集合一般來操作位運算,這樣使用代碼更簡單易懂同時又具備類型安全的優(yōu)勢。注意EnumSet不允許使用 null 元素。試圖插入 null 元素將拋出 NullPointerException,但試圖測試判斷是否存在null 元素或移除 null 元素則不會拋出異常,與大多數(shù)collection 實現(xiàn)一樣,EnumSet不是線程安全的,因此在多線程環(huán)境下應(yīng)該注意數(shù)據(jù)同步問題,ok~,下面先來簡單看看EnumSet的使用方式。
創(chuàng)建EnumSet并不能使用new關(guān)鍵字,因為它是個抽象類,而應(yīng)該使用其提供的靜態(tài)工廠方法,EnumSet的靜態(tài)工廠方法比較多,如下:
創(chuàng)建一個具有指定元素類型的空EnumSet。 EnumSet<E> noneOf(Class<E> elementType) //創(chuàng)建一個指定元素類型并包含所有枚舉值的EnumSet <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) // 創(chuàng)建一個包括枚舉值中指定范圍元素的EnumSet <E extends Enum<E>> EnumSet<E> range(E from, E to) // 初始集合包括指定集合的補(bǔ)集 <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s) // 創(chuàng)建一個包括參數(shù)中所有元素的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) //創(chuàng)建一個包含參數(shù)容器中的所有元素的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創(chuàng)建包含所有枚舉類型的enumSet,其內(nèi)部根據(jù)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("-----------------------------------"); //指定補(bǔ)集,也就是從全部枚舉類型中去除參數(shù)集合中的元素,如下去掉上述enumSet2的元素 EnumSet<Color> enumSet3= EnumSet.complementOf(enumSet2); System.out.println("指定補(bǔ)集:"+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("-----------------------------------"); //復(fù)制enumSet5容器的數(shù)據(jù)作為初始化數(shù)據(jù) 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);//重復(fù)元素 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()); /** 輸出結(jié)果: 添加前:[] 添加后:[GREEN, RED, BLUE, BLACK, YELLOW] ----------------------------------- allOf直接填充:[GREEN, RED, BLUE, BLACK, YELLOW] ----------------------------------- 指定初始化范圍:[BLACK, YELLOW] ----------------------------------- 指定補(bǔ)集:[GREEN, RED, BLUE] ----------------------------------- 指定Color.BLACK元素:[BLACK] 指定Color.BLACK和Color.GREEN元素:[GREEN, BLACK] ----------------------------------- enumSet6:[GREEN, BLACK] ----------------------------------- list:[BLACK, BLACK, RED, BLUE] enumSet7:[RED, BLUE, BLACK] */ } }
關(guān)于Java中枚舉類型的作用是什么問題的解答就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。