扒一扒 Java 中的枚舉類型
前言
在 Java 中, 枚舉, 也稱為枚舉類型, 其是一種特殊的數(shù)據(jù)類型, 它使得變量能夠稱為一組預(yù)定義的常量。 其目的是強(qiáng)制編譯時(shí)類型安全。
枚舉類更加直觀,類型安全。使用常量會有以下幾個缺陷:
1. 類型不安全。若一個方法中要求傳入季節(jié)這個參數(shù),用常量的話,形參就是int類型,開發(fā)者傳入任意類型的int類型值就行,但是如果是枚舉類型的話,就只能傳入枚舉類中包含的對象。
2. 沒有命名空間。開發(fā)者要在命名的時(shí)候以SEASON_開頭,這樣另外一個開發(fā)者再看這段代碼的時(shí)候,才知道這四個常量分別代表季節(jié)。
因此, 在 Java 中, enum 是保留的關(guān)鍵字。
1. 枚舉的定義
在 Java 是在 JDK 1.4 時(shí)決定引入的, 其在 JDK 1.5 發(fā)布時(shí)正式發(fā)布的。
舉一個簡單的例子:以日常生活中的方向來定義, 因?yàn)槠涿Q, 方位等都是確定, 一提到大家就都知道。
1.1 傳統(tǒng)的非枚舉方法
如果不使用枚舉, 我們可能會這樣子定義
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; }
以上的定義也是可以達(dá)到定義的, 我們在使用時(shí)
@Test public void testDirection() { System.out.println(getDirectionName(Direction.EAST)); System.out.println(getDirectionName(5));// 也可以這樣調(diào)用 } 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"; } }
運(yùn)行起來也沒問題。 但是, 我們就如同上面第二種調(diào)用方式一樣, 其實(shí)我們的方向就在 4 種范圍之內(nèi),但在調(diào)用的時(shí)候傳入不是方向的一個 int 類型的數(shù)據(jù), 編譯器是不會檢查出來的。
1.2 枚舉方法
我們使用枚舉來實(shí)現(xiàn)上面的功能
定義
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"; } }
以上只是一個舉的例子, 其實(shí), 枚舉中可以很方便的獲取自己的名稱。
通過使用枚舉, 我們可以很方便的限制了傳入的參數(shù), 如果傳入的參數(shù)不是我們指定的類型, 則就發(fā)生錯誤。
1.3 定義總結(jié)
以剛剛的代碼為例
public enum DirectionEnum { EAST, WEST, NORTH, SOUTH }
- 枚舉類型的定義跟類一樣, 只是需要將 class 替換為 enum
- 枚舉名稱與類的名稱遵循一樣的慣例來定義
- 枚舉值由于是常量, 一般推薦全部是大寫字母
- 多個枚舉值之間使用逗號分隔開
- 最好是在編譯或設(shè)計(jì)時(shí)就知道值的所有類型, 比如上面的方向, 當(dāng)然后面也可以增加
2 枚舉的本質(zhì)
枚舉在編譯時(shí), 編譯器會將其編譯為 Java 中 java.lang.Enum
的子類。
我們將上面的 DirectionEnum 進(jìn)行反編譯, 可以獲得如下的代碼:
// final:無法繼承 public final class DirectionEnum extends Enum { // 在之前定義的實(shí)例 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 方法, 調(diào)用父類的 valueOf 方法 public static DirectionEnum valueOf(String name) { return (DirectionEnum)Enum.valueOf(cn/homejim/java/lang/DirectionEnum, name); } // 私有化構(gòu)造函數(shù), 正常情況下無法從外部進(jìn)行初始化 private DirectionEnum(String s, int i) { super(s, i); } // 靜態(tài)代碼塊初始化枚舉實(shí)例 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 }); } }
通過以上反編譯的代碼, 可以發(fā)現(xiàn)以下幾個特點(diǎn)
2.1 繼承 java.lang.Enum
通過以上的反編譯, 我們知道了, java.lang.Enum
是所有枚舉類型的基類。查看其定義
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
可以看出來, java.lang.Enum 有如下幾個特征
- 抽象類, 無法實(shí)例化
- 實(shí)現(xiàn)了 Comparable 接口, 可以進(jìn)行比較
- 實(shí)現(xiàn)了 Serializable 接口, 可進(jìn)行序列化
因此, 相對應(yīng)的, 枚舉類型也可以進(jìn)行比較和序列化
2.2 final 類型
final 修飾, 說明枚舉類型是無法進(jìn)行繼承的
2.3 枚舉常量本身就是該類的實(shí)例對象
可以看到, 我們定義的常量, 在類內(nèi)部是以實(shí)例對象存在的, 并使用靜態(tài)代碼塊進(jìn)行了實(shí)例化。
2.4 構(gòu)造函數(shù)私有化
不能像正常的類一樣, 從外部 new 一個對象出來。
2.5 添加了 $values[] 變量及兩個方法
- $values[]: 一個類型為枚舉類本身的數(shù)組, 存儲了所有的示例類型
- values() : 獲取以上所有實(shí)例變量的克隆值
- valueOf(): 通過該方法可以通過名稱獲得對應(yīng)的枚舉常量
3 枚舉的一般使用
枚舉默認(rèn)是有幾個方法的
3.1 類本身的方法
從前面我的分析, 我們得出, 類本身有兩個方法, 是編譯時(shí)添加的
3.1.1 values()
先看其源碼
public static DirectionEnum[] values() { return (DirectionEnum[])$VALUES.clone(); }
返回的是枚舉常量的克隆數(shù)組。
使用示例
@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)
該方法通過字符串獲取對應(yīng)的枚舉常量
@Test public void testValueOf() { DirectionEnum east = DirectionEnum.valueOf("EAST"); System.out.println(east.ordinal());// 輸出0 }
3.2 繼承的方法
因?yàn)槊杜e類型繼承于 java.lang.Enum
, 因此除了該類的私有方法, 其他方法都是可以使用的。
3.2.1 ordinal()
該方法返回的是枚舉實(shí)例的在定義時(shí)的順序, 類似于數(shù)組, 第一個實(shí)例該方法的返回值為 0。
在基于枚舉的復(fù)雜數(shù)據(jù)結(jié)構(gòu) EnumSet和EnumMap 中會用到該函數(shù)。
@Test public void testOrdinal() { System.out.println(DirectionEnum.EAST.ordinal());// 輸出 0 System.out.println(DirectionEnum.NORTH.ordinal()); // 輸出 2 }
3.2.2 compareTo()
該方法時(shí)實(shí)現(xiàn)的 Comparable 接口的, 其實(shí)現(xiàn)如下
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()
方法時(shí) final 類型, 是不能被覆蓋的! 而 toString 可以被覆蓋。
3.2.4 getDeclaringClass()
獲取對應(yīng)枚舉類型的 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 枚舉類型進(jìn)階
枚舉類型通過反編譯我們知道, 其實(shí)也是一個類(只不過這個類比較特殊, 加了一些限制), 那么, 在類上能做的一些事情對其也是可以做的。 但是, 個別的可能會有限制(方向吧, 編譯器會提醒我們的)
4.1 自定義構(gòu)造函數(shù)
首先, 定義的構(gòu)造函數(shù)可以是 private, 或不加修飾符
自定義構(gòu)造函數(shù)
我們給每個方向加上一個角度
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 自定義具體方法
我們在枚舉類型內(nèi)部加入如下具體方法
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();
定義完之后, 發(fā)現(xiàn)編譯器報(bào)錯了, 說我們需要實(shí)現(xiàn)這個方法
按要求實(shí)現(xiàn)
測試
@Test public void testAbstractMethod() { System.out.println(DirectionEnum.EAST.onDirection()); System.out.println(DirectionEnum.SOUTH.onDirection()); }
輸出
EAST direction 1
NORTH direction 333
也就是說抽象方法會強(qiáng)制要求每一個枚舉常量自己實(shí)現(xiàn)該方法。 通過提供不同的實(shí)現(xiàn)來達(dá)到不同的目的。
4.3 覆蓋父類方法
在父類 java.lang.Enum 中, 也就只有 toString() 是沒有使用 final 修飾啦, 要覆蓋也只能覆蓋該方法。 該方法的覆蓋相信大家很熟悉, 在此就不做過多的講解啦
4.4 實(shí)現(xiàn)接口
因?yàn)镴ava是單繼承的, 因此, Java中的枚舉因?yàn)橐呀?jīng)繼承了 java.lang.Enum, 因此不能再繼承其他的類。
但Java是可以實(shí)現(xiàn)多個接口的, 因此 Java 中的枚舉也可以實(shí)現(xiàn)接口。
定義接口
public interface TestInterface { void doSomeThing(); }
實(shí)現(xiàn)接口
public enum DirectionEnum implements TestInterface{ // 其他代碼 public void doSomeThing() { System.out.println("doSomeThing Implement"); } // 其他代碼 }
測試
@Test public void testImplement() { DirectionEnum.WEST.doSomeThing(); // 輸出 doSomeThing Implement }
5 使用枚舉實(shí)現(xiàn)單例
該方法是在 《Effective Java》 提出的
public enum Singlton { INSTANCE; public void doOtherThing() { } }
使用枚舉的方式, 保證了序列化機(jī)制, 絕對防止多次序列化問題, 保證了線程的安全, 保證了單例。 同時(shí), 防止了反射的問題。
該方法無論是創(chuàng)建還是調(diào)用, 都是很簡單。 《Effective Java》 對此的評價(jià):
單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法。
6 枚舉相關(guān)的集合類
java.util.EnumSet 和 java.util.EnumMap, 在此不進(jìn)行過多的講述了。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
Java遞歸遍歷樹形結(jié)構(gòu)的實(shí)現(xiàn)代碼
這篇文章主要介紹了Java遞歸遍歷樹形結(jié)構(gòu)的相關(guān)資料,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2016-03-03詳解關(guān)于eclipse中使用jdk15對應(yīng)javafx15的配置問題總結(jié)
這篇文章主要介紹了詳解關(guān)于eclipse中使用jdk15對應(yīng)javafx15的配置問題總結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11Springboot如何根據(jù)實(shí)體類生成數(shù)據(jù)庫表
這篇文章主要介紹了Springboot如何根據(jù)實(shí)體類生成數(shù)據(jù)庫表的操作,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09SpringBoot集成gRPC微服務(wù)工程搭建實(shí)踐的方法
這篇文章主要介紹了SpringBoot集成gRPC微服務(wù)工程搭建實(shí)踐的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-01-01