Java中的枚舉,這一篇全了,一些不爲人知的乾貨

大家好,我是躍哥。在平時編寫代碼的時候,會遇到一些常量,並且這些常量具有規律性,比如:男,女;比如學科:文科、理科;比如一週的天數,週一--週日,你會用魔鬼數字去編輯嗎?

肯定不會吧。有一種使用就叫枚舉。那今天就來看看Java枚舉,以及一些不爲人知的乾貨吧。講真,躍哥平時用枚舉的時候,還得偷偷的去查資料呢!

Java枚舉,也稱作Java枚舉類型,是一種字段由一組固定常量集合組成的類型。枚舉的主要目的是加強編譯時類型的安全性。enum關鍵字是Java中的保留關鍵字。

在編譯或設計時,當我們知道所有變量的可能性時,儘量使用枚舉類型。本篇文章就帶大家全面系統的瞭解枚舉的使用,以及會遇到的一些問題。


Java中的枚舉


枚舉通常是一組相關的常量集合,其他編程語言很早就開始用枚舉了,比如C++。從JDK1.5起,Java也開始支持枚舉類型。

枚舉是一種特殊的數據類型,它既是一種類(class)類型卻又比類類型多了些特殊的約束,這些約束也造就了枚舉類型的簡潔性、安全性以及便捷性。

在Java中,通過enum來聲明枚舉類型,默認繼承自java.lang.Enum。所以聲明枚舉類時無法再繼承其他類。


枚舉聲明


在生活中我們會經常辨認方向,東南西北,它們的名稱、屬性等基本都是確定的,我們就可以將其聲明爲枚舉類型:

public enum Direction {
   EAST, WEST, NORTH, SOUTH;
}

同樣,每週七天也可以聲明成枚舉類型:

enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

在沒有枚舉或沒使用枚舉的情況下,並不是說不可以定義變量,我們可以通過類或接口進行常量的定義:

public class Day {

    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值相同的變量,混淆的機率還是很大的,編譯器也不會提出任何警告。因此,當能使用枚舉的時候,並不提倡這種寫法。


枚舉的底層實現


上面我們已經說了,枚舉是一個特殊的類,每一個枚舉項本質上都是枚舉類自身的實例。

因此,上面枚舉類Direction可以通過下面代碼進行示例:

final class Direction extends Enum{
    public final static Direction EAST = new Direction();
    public final static Direction WEST = new Direction();
    public final static Direction NORTH = new Direction();
    public final static Direction SOUTH = new Direction();
}

首先通過javac命令對Direction進行編譯,然後通過javap命令來查看一下對應class文件內容:

bogon:enums apple$ javap Direction.class 
Compiled from "Direction.java"
public final class com.choupangxia.enums.Direction extends java.lang.Enum<com.choupangxia.enums.Direction> {
  public static final com.choupangxia.enums.Direction EAST;
  public static final com.choupangxia.enums.Direction WEST;
  public static final com.choupangxia.enums.Direction NORTH;
  public static final com.choupangxia.enums.Direction SOUTH;
  public static com.choupangxia.enums.Direction[] values();
  public static com.choupangxia.enums.Direction valueOf(java.lang.String);
  static {};
}

可以看到,一個枚舉在經過編譯器編譯過後,變成了一個抽象類,它繼承了java.lang.Enum;而枚舉中定義的枚舉常量,變成了相應的public static final屬性,而且其類型就抽象類的類型,名字就是枚舉常量的名字。


枚舉使用實例


通過上面的反編譯我們可以看到,枚舉的選項本質上就是public static final的變量,所以就把它當做這樣的變量使用即可。

public class EnumExample {
    public static void main(String[] args) {
        Direction north = Direction.NORTH;
        System.out.println(north);        //Prints NORTH
    }
}


枚舉的ordinal()方法


ordinal()方法用於獲取枚舉變量在枚舉類中聲明的順序,下標從0開始,與數組中的下標很相似。它的設計是用於EumSet和EnumMap複雜的基於枚舉的數據結構使用。

Direction.EAST.ordinal();     //0
 
Direction.NORTH.ordinal();    //2

需要注意的是如果枚舉項聲明的位置發生了變化,那麼ordinal方法的值也隨之變化。所以,進來避免使用該方法。不然,當枚舉項比較多時,別人在中間增刪一項,會導致後續的所有順序變化。


枚舉的values()和valueOf()


values()方法可獲取枚舉類中的所有變量,並作爲數組返回:

Direction[] directions = Direction.values();
 
for (Direction d : directions) {
    System.out.println(d);
}
 
//Output:
 
EAST
WEST
NORTH
SOUTH

values()方法是由編譯器插入到枚舉類中的static方法,而它的父類Enum中並不存在這個方法。

valueOf(String name)方法與Enum類中的valueOf方法的作用類似根據名稱獲取枚舉變量,同樣是由編譯器生成的,但更簡潔些,只需傳遞一個參數。

Direction east = Direction.valueOf("EAST");
         
System.out.println(east);
 
//Output:
 
EAST


枚舉命名約定


按照約定,枚舉屬於常量,因此採用所有字母大寫,下劃線分割的風格(UPPER_CASE)。也就是說枚舉類名與普通類約定一樣,而枚舉中的變量與靜態變量的命名規範一致。


枚舉的構造方法


默認情況下,枚舉類是不需要構造方法的,默認的變量就是聲明時的字符串。當然,你也可以通過自定義構造方法,來初始化枚舉的一些狀態信息。通常情況下,我們會在構造參數中傳入兩個參數,比如,一個編碼,一個描述。

以上面的方向爲例:

public enum Direction {
    // enum fields
    EAST(0), WEST(180), NORTH(90), SOUTH(270);
 
    // constructor
    private Direction(final int angle) {
        this.angle = angle;
    }
 
    // internal state
    private int angle;
 
    public int getAngle() {
        return angle;
    }
}

如果我們想訪問每個方向的角度,可以通過簡單的方法調用:

Direction north = Direction.NORTH;
         
System.out.println(north);                      //NORTH
 
System.out.println(north.getAngle());           //90
 
System.out.println(Direction.NORTH.getAngle()); //90


枚舉中的方法


枚舉就是一個特殊的類,因此也可以像普通的類一樣擁有方法和屬性。在枚舉中不僅可以聲明具體的方法,還可以聲明抽象方法。

方法的訪問權限可以是private、protected和public。可以通過這些方法返回枚舉項的值,也可以做一些內部的私有處理。

public enum Direction {
    // enum fields
    EAST, WEST, NORTH, SOUTH;
     
    protected String printDirection() {
        String message = &quot;You are moving in &quot; + this + &quot; direction&quot;;
        System.out.println( message );
        return message;
    }
}

對應方法的使用如下:

Direction.NORTH.printDirection(); 
Direction.EAST.printDirection(); 

枚舉類中還可以定義抽象的方法,但每個枚舉中必須實現對應的抽象方法:

public enum Direction 
{
    // enum fields
    EAST {
        @Override
        public String printDirection() {
            String message = &quot;You are moving in east. You will face sun in morning time.&quot;;
            return message;
        }
    },
    WEST {
        @Override
        public String printDirection() {
            String message = &quot;You are moving in west. You will face sun in evening time.&quot;;
            return message;
        }
    },
    NORTH {
        @Override
        public String printDirection() {
            String message = &quot;You are moving in north. You will face head in daytime.&quot;;
            return message;
        }
    },
    SOUTH {
        @Override
        public String printDirection() {
            String message = &quot;You are moving in south. Sea ahead.&quot;;
            return message;
        }
    };
 
    public abstract String printDirection();
}

抽象方法的調用,與普通方法一樣:

Direction.NORTH.printDirection(); 
Direction.EAST.printDirection(); 

通過這種方式就可以輕而易舉地定義每個枚舉實例的不同行爲方式。比如需要每個枚舉項都打印出方向的名稱,就可以定義這麼一個抽象的方法。

上面的實例enum類似乎表現出了多態的特性,可惜的是枚舉類型的實例終究不能作爲類型傳遞使用。下面的方式編譯器都無法通過:

//無法通過編譯,Direction.NORTH是個實例對象
 public void text(Direction.NORTH instance){ }


枚舉的繼承


上面已經提到過枚舉繼承自java.lang.Enum,Enum是一個抽象類:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
    // ...
}

也就是說,所有的枚舉類都支持比較(Comparable)和序列化(Serializable)的特性。也正因爲所有的枚舉類都繼承了Enum,所以無法再繼承其他類了,但是可以實現接口。


枚舉的比較


所有的枚舉默認都是Comparable和單例的,因此可以通過equals方法進行比較,甚至可以直接用雙等號“==”進行比較。

Direction east = Direction.EAST;
Direction eastNew = Direction.valueOf("EAST");
 
System.out.println( east == eastNew );           //true
System.out.println( east.equals( eastNew ) );    //true


枚舉集合:EnumSet和EnumMap


在java.util包下引入了兩個枚舉集合類:EnumSet和EnumMap。

EnumSet

EnumSet類的定義如下:

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable{
    // ...
}

EnumSet是與枚舉類型一起使用的專用Set集合,EnumSet中所有元素都必須是枚舉類型。與其他Set接口的實現類HashSet/TreeSet不同的是,EnumSet在內部實現是位向量。

位向量是一種極爲高效的位運算操作,由於直接存儲和操作都是bit,因此EnumSet空間和時間性能都十分可觀,足以媲美傳統上基於int的“位標誌”的運算,關鍵是我們可像操作set集合一般來操作位運算。

EnumSet不允許使用null元素,試圖插入null將拋出 NullPointerException,但測試判斷是否存在null元素或移除null元素則不會拋出異常,與大多數Collection實現一樣,EnumSet不是線程安全的,在多線程環境下需注意數據同步問題。

使用實例:

public class Test {
   public static void main(String[] args) {
     Set enumSet = EnumSet.of(  Direction.EAST,
                                Direction.WEST,
                                Direction.NORTH,
                                Direction.SOUTH
                              );
   }
 }

EnumMap

EnumMap的聲明如下:

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
{}

與EnumSet類似,EnumMap是一個特殊的Map,Map的Key必須是枚舉類型。EnumMap內部是通過數組實現的,效率比普通的Map更高一些。EnumMap的key值不能爲null,並且EnumMap也不是線程安全的。

EnumMap使用實例如下:

public class Test {
  public static void main(String[] args){
    //Keys can be only of type Direction
    Map enumMap = new EnumMap(Direction.class);
 
    //Populate the Map
    enumMap.put(Direction.EAST, Direction.EAST.getAngle());
    enumMap.put(Direction.WEST, Direction.WEST.getAngle());
    enumMap.put(Direction.NORTH, Direction.NORTH.getAngle());
    enumMap.put(Direction.SOUTH, Direction.SOUTH.getAngle());
  }
}


枚舉與switch


使用switch進行條件判斷時,條件參數一般只能是整型,字符型,同時也支持枚舉型,在java7後switch也對字符串進行了支持。

使用實例如下:

enum Color {GREEN,RED,BLUE}

public class EnumDemo4 {

    public static void printName(Color color){
        switch (color){
            //無需使用Color進行引用
            case BLUE: 
                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);
    }
}


枚舉與單例


單例模式是日常使用中最常見的設計模式之一了,單例的實現有很多種實現方法(餓漢模式、懶漢模式等),這裏就不再贅述,只以一個最普通的單例來做對照,進而看看基於枚舉如何來實現單例模式。

餓漢模式的實現:

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

簡單直接,缺點是可能在還不需要時就把實例創建出來了,沒起到lazy loading的效果。優點就是實現簡單,而且安全可靠。

這樣一個單例場景,如果通過枚舉進行實現如下:

public enum Singleton {

    INSTANCE;

    public void doSomething() {
        System.out.println("doSomething");
    }
}

在effective java中說道,最佳的單例實現模式就是枚舉模式。利用枚舉的特性,讓JVM來幫我們保證線程安全和單一實例的問題。除此之外,寫法還特別簡單。

直接通過Singleton.INSTANCE.doSomething()的方式調用即可。方便、簡潔又安全。


小結


枚舉在日常編碼中幾乎是必不可少的,如何用好,如何用精,還需要基礎知識的鋪墊,本文也正是基於此帶大家從頭到尾梳理了一遍。有所收穫就點個贊吧。






0、重磅!兩萬字長文總結,梳理 Java 入門進階哪些事(推薦收藏)

1、二月,拉開牛氣沖天的一年

2、都退稅了嗎?和你聊聊發工資的騷操作。。

3、想蛻變,就必須翻過算法這座山

本文分享自微信公衆號 - 程序員小躍(runningdimple)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章