了解枚舉類型
枚舉類型是Java 5中新增特性的一部分,它是一種特殊的資料類型,之是以特殊是因為它既是一種類(class)類型卻又比類類型多了些特殊的限制,但是這些限制的存在也造就了枚舉類型的簡潔性、安全性以及便捷性。下面先來看看什麼是枚舉?如何定義枚舉?
枚舉的定義
回憶一下下面的程式,這是在沒有枚舉類型時定義常量常見的方式
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;
}
上述的常量定義常量的方式稱為int枚舉模式,這樣的定義方式并沒有什麼錯,但它存在許多不足,如在類型安全和使用友善性上并沒有多少好處,如果存在定義int值相同的變量,混淆的幾率還是很大的,編譯器也不會提出任何警告,是以這種方式在枚舉出現後并不提倡,現在我們利用枚舉類型來重新定義上述的常量,同時也感受一把枚舉定義的方式,如下定義周一到周日的常量
//枚舉類型,使用關鍵字enum
enum Day {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
相當簡潔,在定義枚舉類型時我們使用的關鍵字是enum,與class關鍵字類似,隻不過前者是定義枚舉類型,後者是定義類類型。枚舉類型Day中分别定義了從周一到周日的值,這裡要注意,值一般是大寫的字母,多個值之間以逗号分隔。同時我們應該知道的是枚舉類型可以像類(class)類型一樣,定義為一個單獨的檔案,當然也可以定義在其他類内部,更重要的是枚舉常量在類型安全性和便捷性都很有保證,如果出現類型問題編譯器也會提示我們改進,但務必記住枚舉表示的類型其取值是必須有限的,也就是說每個值都是可以枚舉出來的,比如上述描述的一周共有七天。那麼該如何使用呢?如下:
public class EnumDemo {
public static void main(String[] args){
//直接引用
Day day =Day.MONDAY;
}
}
//定義枚舉類型
enum Day {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
就像上述代碼那樣,直接引用枚舉的值即可,這便是枚舉類型的最簡單模型。
枚舉實作原理
我們大概了解了枚舉類型的定義與簡單使用後,現在有必要來了解一下枚舉類型的基本實作原理。實際上在使用關鍵字enum建立枚舉類型并編譯後,編譯器會為我們生成一個相關的類,這個類繼承了Java API中的java.lang.Enum類,也就是說通過關鍵字enum建立枚舉類型在編譯後事實上也是一個類類型而且該類繼承自java.lang.Enum類。下面我們編譯前面定義的EnumDemo.java并檢視生成的class檔案來驗證這個結論:
//檢視目錄下的java檔案
[email protected] enumdemo$ ls
EnumDemo.java
//利用javac指令編譯EnumDemo.java
[email protected] enumdemo$ javac EnumDemo.java
//檢視生成的class檔案,注意有Day.class和EnumDemo.class 兩個
[email protected] enumdemo$ ls
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是抽象類),以下是它的常見方法:
傳回類型 | 方法名稱 | 方法說明 |
---|---|---|
| | 比較此枚舉與指定對象的順序 |
| | 當指定對象等于此枚舉常量時,傳回 true。 |
| | 傳回與此枚舉常量的枚舉類型相對應的 Class 對象 |
| | 傳回此枚舉常量的名稱,在其枚舉聲明中對其進行聲明 |
| | 傳回枚舉常量的序數(它在枚舉聲明中的位置,其中初始常量序數為零) |
| | 傳回枚舉常量的名稱,它包含在聲明中 |
| | |
這裡主要說明一下
ordinal()
方法,該方法擷取的是枚舉變量在枚舉類中聲明的順序,下标從0開始,如日期中的MONDAY在第一個位置,那麼MONDAY的ordinal值就是0,如果MONDAY的聲明位置發生變化,那麼ordinal方法擷取到的值也随之變化,注意在大多數情況下我們都不應該首先使用該方法,畢竟它總是變幻莫測的。
compareTo(E o)
方法則是比較枚舉的大小,注意其内部實作是根據每個枚舉的ordinal值大小進行比較的。
name()
方法與
toString()
幾乎是等同的,都是輸出變量的字元串形式。至于
valueOf(Class<T> enumType, String name)
方法則是根據枚舉類的Class對象和枚舉名稱擷取枚舉常量,注意該方法是靜态的,後面在枚舉單例時,我們還會詳細分析該方法,下面的代碼示範了上述方法:
package com.zejian.enumdemo;
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對象中存在如下方法:
傳回類型 | 方法名稱 | 方法說明 |
---|---|---|
| | 傳回該枚舉類型的所有元素,如果Class對象不是枚舉類型,則傳回null。 |
| | 當且僅當該類聲明為源代碼中的枚舉時傳回 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方法,傳回中文描述内容,自定義私有構造函數,在聲明枚舉執行個體時傳入對應的中文描述,代碼如下:
package com.zejian.enumdemo;
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方法:
package com.zejian.enumdemo;
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關鍵字對于枚舉類來說并不是必須的如下:
package com.zejian.enumdemo;
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類同樣是可以實作多接口的,如下:
package com.zejian.enumdemo;
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引用。
枚舉與單例模式
單例模式可以說是最常使用的設計模式了,它的作用是確定某個類隻有一個執行個體,自行執行個體化并向整個系統提供這個執行個體。在實際應用中,線程池、緩存、日志對象、對話框對象常被設計成單例,總之,選擇單例模式就是為了避免不一緻狀态,下面我們将會簡單說明單例模式的幾種主要編寫方式,進而對比出使用枚舉實作單例模式的優點。首先看看餓漢式的單例模式:
public class SingletonHungry {
private static SingletonHungry instance = new SingletonHungry();
private SingletonHungry() {
}
public static SingletonHungry getInstance() {
return instance;
}
}
顯然這種寫法比較簡單,但問題是無法做到延遲建立對象,事實上如果該單例類涉及資源較多,建立比較耗時間時,我們更希望它可以盡可能地延遲加載,進而減小初始化的負載,于是便有了如下的懶漢式單例:
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
}
我們有如下解決方案,使用Map集合來統計,key值作為顔色名稱,value代表衣服數量,如下:
package com.zejian.enumdemo;
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;
}
相對應put方法,get方法顯示相當簡潔,key有效的話,直接通過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)
代碼示範如下:
package zejian;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
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]
*/
}
}
noneOf(Class<E> elementType)
靜态方法,主要用于建立一個空的EnumSet集合,傳遞參數elementType代表的是枚舉類型的類型資訊,即Class對象。
EnumSet<E> allOf(Class<E> elementType)
靜态方法則是建立一個填充了elementType類型所代表的所有枚舉執行個體,奇怪的是EnumSet提供了多個重載形式的of方法,最後一個接受的的是可變參數,其他重載方法則是固定參數個數,EnumSet之是以這樣設計是因為可變參數的運作效率低一些,所有在參數資料不多的情況下,強烈不建議使用傳遞參數為可變參數的of方法,即
EnumSet<E> of(E first, E... rest)
,其他方法就不分析了,看代碼示範即可。至于EnumSet的操作方法,則與set集合是一樣的,可以看API即可這也不過多說明。什麼時候使用EnumSet比較恰當的,事實上當需要進行位域運算,就可以使用EnumSet提到位域,如下:
public class EnumSetDemo {
//定義位域變量
public static final int TYPE_ONE = 1 << 0 ; //1
public static final int TYPE_TWO = 1 << 1 ; //2
public static final int TYPE_THREE = 1 << 2 ; //4
public static final int TYPE_FOUR = 1 << 3 ; //8
public static void main(String[] args){
//位域運算
int type= TYPE_ONE | TYPE_TWO | TYPE_THREE |TYPE_FOUR;
}
}
諸如上述情況,我們都可以将上述的類型定義成枚舉然後采用EnumSet來裝載,進行各種操作,這樣不僅不用手動編寫太多備援代碼,而且使用EnumSet集合進行操作也将使代碼更加簡潔明了。
enum Type{
TYPE_ONE,TYPE_TWO,TYPE_THREE,TYPE_FOUR
}
public class EnumSetDemo {
public static void main(String[] args){
EnumSet set =EnumSet.of(Type.TYPE_ONE,Type.TYPE_FOUR);
}
}
其實部落客認為EnumSet最有價值的是其内部實作原理,采用的是位向量,它展現出來的是一種高效的資料處理方式,這點很值得我們去學習它。
EnumSet實作原理剖析
關于EnumSet實作原理可能會有點燒腦,内部執行幾乎都是位運算,部落客将盡力使用圖檔來分析,協助大家了解。
了解位向量
在分析EnumSet前有必要先了解以下位向量,顧名思義位向量就是用一個bit位(0或1)标記一個元素的狀态,用一組bit位表示一個集合的狀态,而每個位對應一個元素,每個bit位的狀态隻可能有兩種,即0或1。位向量能表示的元素個數與向量的bit位長度有關,如一個int類型能表示32個元素,而一個long類型則可以表示64個元素,對于EnumSet而言采用的就long類型或者long類型數組。比如現在有一個檔案中的資料,該檔案存儲了N=1000000個無序的整數,需要把這些整數讀取到記憶體并排序再重新寫回檔案中,該如何解決?最簡單的方式是用int類型來存儲每個數,并把其存入到數組(int a[m])中,再進行排序,但是這種方式将會導緻存儲空間異常大,對資料操作起來效率也能成問題,那有沒更高效的方式呢?的确是有的,那就是運用位向量,我們知道一個int型的數有4個位元組,也就是32位,那麼我們可以用N/32個int型數組來表示這N個數:
a[0]表示第1~32個數(0~31)
a[1]表示第33~64個數(32~63)
a[2]表示第65~96個數(64~95)
...... 以此類推
這樣,每當輸入一個數字m,我們應該先找到該數字在數組的第?個元素,也就是a[?],然後再确定在這個元素的第幾個bit位,找到後設定為1,代表存在該數字。舉個例子來說,比如輸入40,那麼40/32為1餘8,則應該将a[1]元素值的第9個bit位置為1(1的二進制左移8位後就是第9個位置),表示該數字存在,40數字的表示原理圖過程如下:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICdzFWRoRXdvN1LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX90zZihGbtFGbwhVWyYUbhZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39DM4QzMzUTN4AzMxUDM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
大概明白了位向量表示方式後,上述過程的計算方式,通過以下方式可以計算該數存儲在數組的第?個元素和元素中第?個bit位置,為了示範友善,我們這裡假設整第?個元素中的?為P,餘值設定S
//m 除以 2^n 則商(P)表示為 m >> n
//等同于 m / 2^5 取整數 即:40 / 32 = 1 ,那麼P=1就是數組中第2個元素,即a[1]
//位操作過程如下,40的二進制
00000000 00000000 00000000 00101000
//右移5位即 n=5 , m >> 5 ,即結果轉為10進制就是P=1
00000000 00000000 00000000 00000001
在這裡我們使用的int類型,即32位,所有2^5=32,是以n=5,由此計算出 P的值代表的是數組的第 P 個元素,接着利用下述方式計算出餘數(S),以此設定該元素值的第(S+1)個bit位為1
//m 除以2^n 的餘數(S)表示為 m & (2^n-1)
//等同于: m % 2^5 取餘數 即:40 % 32 = 8
//m=40的二進制
00000000 00000000 00000000 00101000
//2^n-1(31)的二進制
00000000 00000000 00000000 00011111
// m & (2^n-1) 即40與31進行與操作得出餘數 即 S=8
00000000 00000000 00000000 00001000
//下面是将a[1]元素值的第(8+1)個bit設定為1,為什麼是(8+1)不是8?因為1左移8位就在第9個bit位了,過程如下:
//1的二進制如下:
00000000 00000000 00000000 00000001
//1 << 8 利用餘數8對1進行左移動
00000000 00000000 00000001 0000000
//然後再與a[1]執行或操作後就可以将對應的bit位設定為1
//a[P] |= 1 << S 見下述java實作的代碼
通過上述二進制位運算過程(關于位運算可以看部落客的另一篇博文~java位運算)就可以計算出整數部分P和餘數部分S,并成功設定bit位為1,現在利用java來實作這個運算過程如下:
//定義變量
private int[] a; //數組存儲元素的數組
private int BIT_LENGTH = 32;//預設使用int類型
private int P; //整數部分
private int S; //餘數
private int MASK = 0x1F;// 2^5 - 1
private int SHIFT = 5; // 2^n SHIFT=n=5 表示2^5=32 即bit位長度32
計算代碼
/**
* 置位操作,添加操作
* @param i
*/
public void set(int i){
P = i >> SHIFT; //結果等同 P = i / BIT_LENGTH; 取整數 ①
S = i & MASK; //結果等同 S = i % BIT_LENGTH; 取餘數 ②
a[P] |= 1 << S; //指派設定該元素bit位為1 ③
//将int型變量j的第k個比特位設定為1, 即j=j|(1<<k),上述3句合并為一句
//a[i >> SHIFT ] |= (1 << (i & MASK)); ④
}
計算出P和S後,就可以進行指派了,其中 a[P]代表數組中第P個元素,
a[P] |= 1 << S
整句意思是把a[P]元素的第S+1位設定為1,注意從低位到高位設定,即從右到左,①②③合并為④,代碼将更佳簡潔。當然有添加操作,那麼就會有删除操作,删除操作過程與添加類似,隻不過删除是把相對應的bit位設定0,代表不存在該數值。
/**
* 置0操作,相當于清除元素
* @param i
*/
public void clear(int i){
P = i >> SHIFT; //計算位于數組中第?個元素 P = i / BIT_LENGTH;
S = i & MASK; //計算餘數 S = i % BIT_LENGTH;
//把a[P]元素的第S+1個(從低位到高位)bit位設定為0
a[P] &= ~(1 << S);
//更優寫法
//将int型變量j的第k個比特位設定為0,即j= j&~(1<<k)
//a[i>>SHIFT] &= ~(1<<(i &MASK));
}
與添加唯一不同的是,計算出餘數S,利用1左移S位,再取反(~)操作,最後進行與(&)操作,即将a[P]元素的第S+1個(從低位到高位)bit位設定為0,表示删除該數字,這個計算過程大家可以自行推算一下。這就是位向量表示法的添加和清除方法,然後我們可以利用下述的get方法判斷某個bit是否存在某個數字:
/**
* 讀取操作,傳回1代表該bit位有值,傳回0代表該bit位沒值
* @param i
* @return
*/
public int get(int i){
//a[i>>SHIFT] & (1<<(i&MASK));
P = i >> SHIFT;
S = i & MASK;
return Integer.bitCount(a[P] & (1 << S));
}
其中Integer.bitCount()是傳回指定 int 值的二進制補碼(計算機數字的二進制表示法都是使用補碼表示的)表示形式的 1 位的數量。位向量運算整體代碼實作如下:
package com.zejian;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 位向量存儲資料
*/
public class BitVetory {
private int count;
private int[] a; //數組
private int BIT_LENGTH = 32;//預設使用int類型
private int P; //整數部分
private int S; //餘數
private int MASK = 0x1F;// 2^5 - 1
private int SHIFT = 5; // 2^n SHIFT=n=5 表示2^5=32 即bit位長度32
/**
* 初始化位向量
* @param count
*/
public BitVetory(int count) {
this.count = count;
a = new int[(count-1)/BIT_LENGTH + 1];
init();
}
/**
* 将數組中元素bit位設定為0
*/
public void init(){
for (int i = 0; i < count; i++) {
clear(i);
}
}
/**
* 擷取排序後的數組
* @return
*/
public List<Integer> getSortedArray(){
List<Integer> sortedArray = new ArrayList<Integer>();
for (int i = 0; i < count; i++) {
if (get(i) == 1) {//判斷i是否存在
sortedArray.add(i);
}
}
return sortedArray;
}
/**
* 置位操作,設定元素
* @param i
*/
public void set(int i){
P = i >> SHIFT; //P = i / BIT_LENGTH; 取整數
S = i & MASK; //S = i % BIT_LENGTH; 取餘數
a[P] |= 1 << S;
//将int型變量j的第k個比特位設定為1, 即j=j|(1<<k),上述3句合并為一句
//a[i >> SHIFT ] |= (1 << (i & MASK));
}
/**
* 置0操作,相當于清除元素
* @param i
*/
public void clear(int i){
P = i >> SHIFT; //計算位于數組中第?個元素 P = i / BIT_LENGTH;
S = i & MASK; //計算餘數 S = i % BIT_LENGTH;
a[P] &= ~(1 << S);
//更優寫法
//将int型變量j的第k個比特位設定為0,即j= j&~(1<<k)
//a[i>>SHIFT] &= ~(1<<(i &MASK));
}
/**
* 讀取操作,傳回1代表該bit位有值,傳回0代表該bit位沒值
* @param i
* @return
*/
public int get(int i){
//a[i>>SHIFT] & (1<<(i&MASK));
P = i >> SHIFT;
S = i & MASK;
return Integer.bitCount(a[P] & (1 << S));
}
//測試
public static void main(String[] args) {
int count = 25;
List<Integer> randoms = getRandomsList(count);
System.out.println("排序前:");
BitVetory bitVetory = new BitVetory(count);
for (Integer e : randoms) {
System.out.print(e+",");
bitVetory.set(e);
}
List<Integer> sortedArray = bitVetory.getSortedArray();
System.out.println();
System.out.println("排序後:");
for (Integer e : sortedArray) {
System.out.print(e+",");
}
/**
輸出結果:
排序前:
6,3,20,10,18,15,19,16,13,4,21,22,24,2,14,5,12,7,23,8,1,17,9,11,
排序後:
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,
*/
}
private static List<Integer> getRandomsList(int count) {
Random random = new Random();
List<Integer> randomsList = new ArrayList<Integer>();
while(randomsList.size() < (count - 1)){
int element = random.nextInt(count - 1) + 1;//element ∈ [1,count)
if (!randomsList.contains(element)) {
randomsList.add(element);
}
}
return randomsList;
}
}
EnumSet原理
有前面位向量的分析,對于了解EnumSet的實作原理就相對簡單些了,EnumSet内部使用的位向量實作的,前面我們說過EnumSet是一個抽象類,事實上它存在兩個子類,RegularEnumSet和JumboEnumSet。RegularEnumSet使用一個long類型的變量作為位向量,long類型的位長度是64,是以可以存儲64個枚舉執行個體的标志位,一般情況下是夠用的了,而JumboEnumSet使用一個long類型的數組,當枚舉個數超過64時,就會采用long數組的方式存儲。先看看EnumSet内部的資料結構:
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
implements Cloneable, java.io.Serializable
{
//表示枚舉類型
final Class<E> elementType;
//存儲該類型資訊所表示的所有可能的枚舉執行個體
final Enum<?>[] universe;
//..........
}
EnumSet中有兩個變量,一個elementType用于表示枚舉的類型資訊,universe是數組類型,存儲該類型資訊所表示的所有可能的枚舉執行個體,EnumSet是抽象類,是以具體的實作是由子類完成的,下面看看
noneOf(Class<E> elementType)
靜态建構方法
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
//根據EnumMap中的一樣,擷取所有可能的枚舉執行個體
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
//枚舉個數小于64,建立RegularEnumSet
return new RegularEnumSet<>(elementType, universe);
else
//否則建立JumboEnumSet
return new JumboEnumSet<>(elementType, universe);
}
從源碼可以看出如果枚舉值個數小于等于64,則靜态工廠方法中建立的就是RegularEnumSet,否則大于64的話就建立JumboEnumSet。無論是RegularEnumSet還是JumboEnumSet,其構造函數内部都間接調用了EnumSet的構造函數,是以最終的elementType和universe都傳遞給了父類EnumSet的内部變量。如下:
//RegularEnumSet構造
RegularEnumSet(Class<E>elementType, Enum<?>[] universe) {
super(elementType, universe);
}
//JumboEnumSet構造
JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
super(elementType, universe);
elements = new long[(universe.length + 63) >>> 6];
}
在RegularEnumSet類和JumboEnumSet類中都存在一個elements變量,用于記錄位向量的操作,
//RegularEnumSet
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
private static final long serialVersionUID = 3411599620347842686L;
//通過long類型的elements記錄位向量的操作
private long elements = 0L;
//.......
}
//對于JumboEnumSet則是:
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
private static final long serialVersionUID = 334349849919042784L;
//通過long數組類型的elements記錄位向量
private long elements[];
//表示集合大小
private int size = 0;
//.............
}
在RegularEnumSet中elements是一個long類型的變量,共有64個bit位,是以可以記錄64個枚舉常量,當枚舉常量的數量超過64個時,将使用JumboEnumSet,elements在該類中是一個long型的數組,每個數組元素都可以存儲64個枚舉常量,這個過程其實與前面位向量的分析是同樣的道理,隻不過前面使用的是32位的int類型,這裡使用的是64位的long類型罷了。接着我們看看EnumSet是如何添加資料的,RegularEnumSet中的add實作如下
public boolean add(E e) {
//檢測是否為枚舉類型
typeCheck(e);
//記錄舊elements
long oldElements = elements;
//執行位向量操作,是不是很熟悉?
//數組版:a[i >> SHIFT ] |= (1 << (i & MASK))
elements |= (1L << ((Enum)e).ordinal());
return elements != oldElements;
}
關于
elements |= (1L << ((Enum)e).ordinal());
這句跟我們前面分析位向量操作是相同的原理,隻不過前面分析的是數組類型實作,這裡用的long類型單一變量實作,
((Enum)e).ordinal()
通過該語句擷取要添加的枚舉執行個體的序号,然後通過1左移再與 long類型的elements進行或操作,就可以把對應位置上的bit設定為1了,也就代表該枚舉執行個體存在。圖示示範過程如下,注意universe數組在EnumSet建立時就初始化并填充了所有可能的枚舉執行個體,而elements值的第n個bit位1時代表枚舉存在,而擷取的則是從universe數組中的第n個元素值。
這就是枚舉執行個體的添加過程和擷取原理。而對于JumboEnumSet的add實作則是如下:
public boolean add(E e) {
typeCheck(e);
//計算ordinal值
int eOrdinal = e.ordinal();
int eWordNum = eOrdinal >>> 6;
long oldElements = elements[eWordNum];
//與前面分析的位向量相同:a[i >> SHIFT ] |= (1 << (i & MASK))
elements[eWordNum] |= (1L << eOrdinal);
boolean result = (elements[eWordNum] != oldElements);
if (result)
size++;
return result;
}
關于JumboEnumSet的add實作與RegularEnumSet差別是一個是long數組類型,一個long變量,運算原理相同,數組的位向量運算與前面分析的是相同的,這裡不再分析。接着看看如何删除元素
//RegularEnumSet類實作
public boolean remove(Object e) {
if (e == null)
return false;
Class eClass = e.getClass();
if (eClass != elementType && eClass.getSuperclass() != elementType)
return false;
long oldElements = elements;
//将int型變量j的第k個比特位設定為0,即j= j&~(1<<k)
//數組類型:a[i>>SHIFT] &= ~(1<<(i &MASK));
elements &= ~(1L << ((Enum)e).ordinal());//long周遊類型操作
return elements != oldElements;
}
//JumboEnumSet類的remove實作
public boolean remove(Object e) {
if (e == null)
return false;
Class<?> eClass = e.getClass();
if (eClass != elementType && eClass.getSuperclass() != elementType)
return false;
int eOrdinal = ((Enum<?>)e).ordinal();
int eWordNum = eOrdinal >>> 6;
long oldElements = elements[eWordNum];
//與a[i>>SHIFT] &= ~(1<<(i &MASK));相同
elements[eWordNum] &= ~(1L << eOrdinal);
boolean result = (elements[eWordNum] != oldElements);
if (result)
size--;
return result;
}
删除remove的實作,跟位向量的清空操作是同樣的實作原理,如下:
至于JumboEnumSet的實作原理也是類似的,這裡不再重複。下面為了簡潔起見,我們以RegularEnumSet類的實作作為源碼分析,畢竟JumboEnumSet的内部實作原理可以說跟前面分析過的位向量幾乎一樣。o~,看看如何判斷是否包含某個元素
public boolean contains(Object e) {
if (e == null)
return false;
Class eClass = e.getClass();
if (eClass != elementType && eClass.getSuperclass() != elementType)
return false;
//先左移再按&操作
return (elements & (1L << ((Enum)e).ordinal())) != 0;
}
public boolean containsAll(Collection<?> c) {
if (!(c instanceof RegularEnumSet))
return super.containsAll(c);
RegularEnumSet<?> es = (RegularEnumSet<?>)c;
if (es.elementType != elementType)
return es.isEmpty();
//~elements取反相當于elements補集,再與es.elements進行&操作,如果為0,
//就說明elements補集與es.elements沒有交集,也就是es.elements是elements的子集
return (es.elements & ~elements) == 0;
}
對于contains(Object e) 方法,先左移再按位與操作,不為0,則表示包含該元素,跟位向量的get操作實作原理類似,這個比較簡單。對于
containsAll(Collection<?> c)
則可能比較難懂,這裡分析一下,elements變量(long類型)标記EnumSet集合中已存在元素的bit位,如果bit位為1則說明存在枚舉執行個體,為0則不存在,現在執行
~elements
操作後 則說明
~elements
是elements的補集,那麼隻要傳遞進來的es.elements與補集
~elements
執行&操作為0,那麼就可以證明es.elements與補集
~elements
沒有交集的可能,也就是說es.elements隻能是elements的子集,這樣也就可以判斷出目前EnumSet集合中包含傳遞進來的集合c了,借着下圖協助了解:
圖中,elements代表A,
es.elements
代表S,
~elements
就是求A的補集,
(es.elements & ~elements) == 0
就是在驗證A’∩B是不是空集,即S是否為A的子集。接着看retainAll方法,求兩個集合交集
public boolean retainAll(Collection<?> c) {
if (!(c instanceof RegularEnumSet))
return super.retainAll(c);
RegularEnumSet<?> es = (RegularEnumSet<?>)c;
if (es.elementType != elementType) {
boolean changed = (elements != 0);
elements = 0;
return changed;
}
long oldElements = elements;
//執行與操作,求交集,比較簡單
elements &= es.elements;
return elements != oldElements;
}
最後來看看疊代器是如何取值的
public Iterator<E> iterator() {
return new EnumSetIterator<>();
}
private class EnumSetIterator<E extends Enum<E>> implements Iterator<E> {
//記錄elements
long unseen;
//記錄最後一個傳回值
long lastReturned = 0;
EnumSetIterator() {
unseen = elements;
}
public boolean hasNext() {
return unseen != 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (unseen == 0)
throw new NoSuchElementException();
//取值過程,先與本身負執行&操作得出的就是二進制低位開始的第一個1的數值大小
lastReturned = unseen & -unseen;
//取值後減去已取得lastReturned
unseen -= lastReturned;
//傳回在指定 long 值的二進制補碼表示形式中最低位(最右邊)的 1 位之後的零位的數量
return (E) universe[Long.numberOfTrailingZeros(lastReturned)];
}
public void remove() {
if (lastReturned == 0)
throw new IllegalStateException();
elements &= ~lastReturned;
lastReturned = 0;
}
}
比較晦澀的應該是
//取值過程,先與本身負執行&操作得出的就是二進制低位開始的第一個1的數值大小
lastReturned = unseen & -unseen;
//取值後減去已取得lastReturned
unseen -= lastReturned;
return (E) universe[Long.numberOfTrailingZeros(lastReturned)];
我們通過原理圖來協助了解,現在假設集合中已儲存所有可能的枚舉執行個體變量,我們需要把它們周遊展示出來,下面的第一個枚舉元素的擷取過程,顯然通過
unseen & -unseen;
操作,我們可以擷取到二進制低位開始的第一個1的數值,該計算的結果是要麼全部都是0,要麼就隻有一個1,然後指派給lastReturned,通過
Long.numberOfTrailingZeros(lastReturned)
擷取到該bit為1在64位的long類型中的位置,即從低位算起的第幾個bit,如圖,該bit的位置恰好是低位的第1個bit位置,也就指明了universe數組的第一個元素就是要擷取的枚舉變量。執行
unseen -= lastReturned;
後繼續進行第2個元素的周遊,依次類推周遊出所有值,這就是EnumSet的取值過程,真正存儲枚舉變量的是universe數組,而通過long類型變量的bit位的0或1表示存儲該枚舉變量在universe數組的那個位置,這樣做的好處是任何操作都是執行long類型變量的bit位操作,這樣執行效率将特别高,畢竟是二進制直接執行,隻有最終擷取值時才會操作到數組universe。
ok~,到這關于EnumSet的實作原理主要部分我們就分析完了,其内部使用位向量,存儲結構很簡潔,節省空間,大部分操作都是按位運算,直接操作二進制資料,是以效率極高。當然通過前面的分析,我們也掌握位向量的運算原理。好~,關于java枚舉,我們暫時聊到這。
參考資料 《Thinking in Java》 And 《Effective Java》