扒一扒: Java 中的列舉
在 Java 中, 列舉, 也稱為列舉型別, 其是一種特殊的資料型別, 它使得變數能夠稱為一組預定義的常量。 其目的是強制編譯時型別安全。
因此, 在 Java 中, enum 是保留的關鍵字。
1. 列舉的定義
在 Java 是在 JDK 1.4 時決定引入的, 其在 JDK 1.5 釋出時正式釋出的。
舉一個簡單的例子:以日常生活中的方向來定義, 因為其名稱, 方位等都是確定, 一提到大家就都知道。
1.1 傳統的非列舉方法
如果不使用列舉, 我們可能會這樣子定義
public class Direction { public static final int EAST = 0; public static final int WEST = 1; public static final int SOUTH = 2; public static final int NORTH = 3; }
以上的定義也是可以達到定義的, 我們在使用時
@Test public void testDirection() { System.out.println(getDirectionName(Direction.EAST)); System.out.println(getDirectionName(5));// 也可以這樣呼叫 } public String getDirectionName(int type) { switch (type) { case Direction.EAST: return "EAST"; case Direction.WEST: return "WEST"; case Direction.SOUTH: return "SOUTH"; case Direction.NORTH: return "NORTH"; default: return "UNKNOW"; } }
執行起來也沒問題。 但是, 我們就如同上面第二種呼叫方式一樣, 其實我們的方向就在 4 種範圍之內,但在呼叫的時候傳入不是方向的一個 int 型別的資料, 編譯器是不會檢查出來的。
1.2 列舉方法
我們使用列舉來實現上面的功能
定義
public enum DirectionEnum { EAST, WEST, NORTH, SOUTH }
測試
@Test public void testDirectionEnum() { System.out.println(getDirectionName(DirectionEnum.EAST)); // System.out.println(getDirectionName(5));// 編譯錯誤 } public String getDirectionName(DirectionEnum direction) { switch (direction) { case EAST: return "EAST"; case WEST: return "WEST"; case SOUTH: return "SOUTH"; case NORTH: return "NORTH"; default: return "UNKNOW"; } }
以上只是一個舉的例子, 其實, 列舉中可以很方便的獲取自己的名稱。
通過使用列舉, 我們可以很方便的限制了傳入的引數, 如果傳入的引數不是我們指定的型別, 則就發生錯誤。
1.3 定義總結
以剛剛的程式碼為例
public enum DirectionEnum { EAST, WEST, NORTH, SOUTH }
- 列舉型別的定義跟類一樣, 只是需要將 class 替換為 enum
- 列舉名稱與類的名稱遵循一樣的慣例來定義
- 列舉值由於是常量, 一般推薦全部是大寫字母
- 多個列舉值之間使用逗號分隔開
- 最好是在編譯或設計時就知道值的所有型別, 比如上面的方向, 當然後面也可以增加
2 列舉的本質
列舉在編譯時, 編譯器會將其編譯為 Java 中 java.lang.Enum
的子類。
我們將上面的 DirectionEnum
進行反編譯, 可以獲得如下的程式碼:
// final:無法繼承 public final class DirectionEnum extends Enum { // 在之前定義的例項 public static final DirectionEnum EAST; public static final DirectionEnum WEST; public static final DirectionEnum NORTH; public static final DirectionEnum SOUTH; private static final DirectionEnum $VALUES[]; // 編譯器新增的 values() 方法 public static DirectionEnum[] values() { return (DirectionEnum[])$VALUES.clone(); } // 編譯器新增的 valueOf 方法, 呼叫父類的 valueOf 方法 public static DirectionEnum valueOf(String name) { return (DirectionEnum)Enum.valueOf(cn/homejim/java/lang/DirectionEnum, name); } // 私有化建構函式, 正常情況下無法從外部進行初始化 private DirectionEnum(String s, int i) { super(s, i); } // 靜態程式碼塊初始化列舉例項 static { EAST = new DirectionEnum("EAST", 0); WEST = new DirectionEnum("WEST", 1); NORTH = new DirectionEnum("NORTH", 2); SOUTH = new DirectionEnum("SOUTH", 3); $VALUES = (new DirectionEnum[] { EAST, WEST, NORTH, SOUTH }); } }
通過以上反編譯的程式碼, 可以發現以下幾個特點
2.1 繼承 java.lang.Enum
通過以上的反編譯, 我們知道了, java.lang.Enum
是所有列舉型別的基類。檢視其定義
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
可以看出來, java.lang.Enum
有如下幾個特徵
Comparable Serializable
因此, 相對應的, 列舉型別也可以進行比較和序列化
2.2 final 型別
final 修飾, 說明列舉型別是無法進行繼承的
2.3 列舉常量本身就是該類的例項物件
可以看到, 我們定義的常量, 在類內部是以例項物件存在的, 並使用靜態程式碼塊進行了例項化。
2.4 建構函式私有化
不能像正常的類一樣, 從外部 new 一個物件出來。
2.5 添加了 $values[] 變數及兩個方法
- $values[]: 一個型別為列舉類本身的陣列, 儲存了所有的示例型別
- values() : 獲取以上所有例項變數的克隆值
- valueOf(): 通過該方法可以通過名稱獲得對應的列舉常量
3 列舉的一般使用
列舉預設是有幾個方法的
3.1 類本身的方法
從前面我的分析, 我們得出, 類本身有兩個方法, 是編譯時新增的
3.1.1 values()
先看其原始碼
public static DirectionEnum[] values() { return (DirectionEnum[])$VALUES.clone(); }
返回的是列舉常量的克隆陣列。
使用示例
@Test public void testValus() { DirectionEnum[] values = DirectionEnum.values(); for (DirectionEnum direction: values) { System.out.println(direction); } }
輸出
EAST WEST NORTH SOUTH
3.1.2 valueOf(String)
該方法通過字串獲取對應的列舉常量
@Test public void testValueOf() { DirectionEnum east = DirectionEnum.valueOf("EAST"); System.out.println(east.ordinal());// 輸出0 }
3.2 繼承的方法
因為列舉型別繼承於 java.lang.Enum
, 因此除了該類的私有方法, 其他方法都是可以使用的。
3.2.1 ordinal()
該方法返回的是列舉例項的在定義時的順序, 類似於陣列, 第一個例項該方法的返回值為 0。
在基於列舉的複雜資料結構 EnumSet
和 EnumMap
中會用到該函式。
@Test public void testOrdinal() { System.out.println(DirectionEnum.EAST.ordinal());// 輸出 0 System.out.println(DirectionEnum.NORTH.ordinal()); // 輸出 2 }
3.2.2 compareTo()
該方法時實現的 Comparable
介面的, 其實現如下
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 來得出大於、小於還是等於。
@Test public void testCompareTo() { System.out.println(DirectionEnum.EAST.compareTo(DirectionEnum.EAST) == 0);// true System.out.println(DirectionEnum.WEST.compareTo(DirectionEnum.EAST) > 0); // true System.out.println(DirectionEnum.WEST.compareTo(DirectionEnum.SOUTH) < 0); // true }
3.2.3 name() 和 toString()
該兩個方法都是返回列舉常量的名稱。 但是, name() 方法時 final 型別, 是不能被覆蓋的! 而 toString 可以被覆蓋。
3.2.4 getDeclaringClass()
獲取對應列舉型別的 Class 物件
@Test public void testGetDeclaringClass() { System.out.println(DirectionEnum.WEST.getDeclaringClass()); // 輸出 class cn.homejim.java.lang.DirectionEnum }
2.3.5 equals
判斷指定物件與列舉常量是否相同
@Test public void testEquals() { System.out.println(DirectionEnum.WEST.equals(DirectionEnum.EAST)); // false System.out.println(DirectionEnum.WEST.equals(DirectionEnum.WEST)); // true }
4 列舉型別進階
列舉型別通過反編譯我們知道, 其實也是一個類(只不過這個類比較特殊, 加了一些限制), 那麼, 在類上能做的一些事情對其也是可以做的。 但是, 個別的可能會有限制(方向吧, 編譯器會提醒我們的)
4.1 自定義建構函式
首先, 定義的建構函式可以是 private , 或不加修飾符
我們給每個方向加上一個角度
public enum DirectionEnum { EAST(0), WEST(180), NORTH(90), SOUTH(270); private int angle; DirectionEnum(int angle) { this.angle = angle; } public int getAngle() { return angle; } }
測試
@Test public void testConstructor() { System.out.println(DirectionEnum.WEST.getAngle()); // 180 System.out.println(DirectionEnum.EAST.getAngle()); // 0 }
4.2 新增自定義的方法
以上的 getAngle 就是我們新增的自定義的方法
4.2.1 自定義具體方法
我們在列舉型別內部加入如下具體方法
protected void move() { System.out.println("You are moving to " + this + " direction"); }
測試
@Test public void testConcreteMethod() { DirectionEnum.WEST.move(); DirectionEnum.NORTH.move(); }
輸出
You are moving to WEST direction You are moving to NORTH direction
4.2.2 在列舉中定義抽象方法
在列舉型別中, 也是可以定義 abstract 方法的
我們在 DirectinEnum
中定義如下的抽象方法
abstract String onDirection();
定義完之後, 發現編譯器報錯了, 說我們需要實現這個方法
按要求實現
測試
@Test public void testAbstractMethod() { System.out.println(DirectionEnum.EAST.onDirection()); System.out.println(DirectionEnum.SOUTH.onDirection()); }
輸出
EAST direction 1 NORTH direction 333
也就是說抽象方法會強制要求每一個列舉常量自己實現該方法。 通過提供不同的實現來達到不同的目的。
4.3 覆蓋父類方法
在父類 java.lang.Enum
中, 也就只有 toString() 是沒有使用 final 修飾啦, 要覆蓋也只能覆蓋該方法。 該方法的覆蓋相信大家很熟悉, 在此就不做過多的講解啦
4.4 實現介面
因為Java是單繼承的, 因此, Java中的列舉因為已經繼承了 java.lang.Enum
, 因此不能再繼承其他的類。
但Java是可以實現多個介面的, 因此 Java 中的列舉也可以實現介面。
定義介面
public interface TestInterface { void doSomeThing(); }
實現介面
public enum DirectionEnum implements TestInterface{ // 其他程式碼 public void doSomeThing() { System.out.println("doSomeThing Implement"); } // 其他程式碼 }
測試
@Test public void testImplement() { DirectionEnum.WEST.doSomeThing(); // 輸出 doSomeThing Implement }
5 使用列舉實現單例
該方法是在 《Effective Java》 提出的
public enum Singlton { INSTANCE; public void doOtherThing() { } }
使用列舉的方式, 保證了序列化機制, 絕對防止多次序列化問題, 保證了執行緒的安全, 保證了單例。 同時, 防止了反射的問題。
該方法無論是建立還是呼叫, 都是很簡單。 《Effective Java》 對此的評價:
單元素的列舉型別已經成為實現Singleton的最佳方法。
6 列舉相關的集合類
java.util.EnumSet
和 java.util.EnumMap
, 在此不進行過多的講述了。