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

語言: CN / TW / HK

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

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

Java中的列舉

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

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

在Java中,通過enum來宣告列舉型別,預設繼承自java.lang.Enum。所以宣告列舉類時無法再繼承其他類。

列舉宣告

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

Plain Text
 
 
 
 
1
public enum Direction {
2
   EAST, WEST, NORTH, SOUTH;
3
}
4
 
 

同樣,每週七天也可以宣告成列舉型別:

Plain Text
 
 
 
 
1
enum Day {
2
    MONDAY, TUESDAY, WEDNESDAY,
3
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
4
}
5
 
 

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

Plain Text
 
 
 
 
1
public class Day {
2
3
    public static final int MONDAY =1;
4
5
    public static final int TUESDAY=2;
6
7
    public static final int WEDNESDAY=3;
8
9
    public static final int THURSDAY=4;
10
11
    public static final int FRIDAY=5;
12
13
    public static final int SATURDAY=6;
14
15
    public static final int SUNDAY=7;
16
17
}
18
 
 

但這樣存在許多不足,如在型別安全和使用方便性上。如果存在定義int值相同的變數,混淆的機率還是很大的,編譯器也不會提出任何警告。因此,當能使用列舉的時候,並不提倡這種寫法。

列舉的底層實現

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

因此,上面列舉類Direction可以通過下面程式碼進行示例:

Plain Text
 
 
 
 
1
final class Direction extends Enum{
2
    public final static Direction EAST = new Direction();
3
    public final static Direction WEST = new Direction();
4
    public final static Direction NORTH = new Direction();
5
    public final static Direction SOUTH = new Direction();
6
}
7
 
 

首先通過javac命令對Direction進行編譯,然後通過javap命令來檢視一下對應class檔案內容:

Plain Text
 
 
 
 
1
bogon:enums apple$ javap Direction.class 
2
Compiled from "Direction.java"
3
public final class com.choupangxia.enums.Direction extends java.lang.Enum<com.choupangxia.enums.Direction> {
4
  public static final com.choupangxia.enums.Direction EAST;
5
  public static final com.choupangxia.enums.Direction WEST;
6
  public static final com.choupangxia.enums.Direction NORTH;
7
  public static final com.choupangxia.enums.Direction SOUTH;
8
  public static com.choupangxia.enums.Direction[] values();
9
  public static com.choupangxia.enums.Direction valueOf(java.lang.String);
10
  static {};
11
}
12
 
 

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

列舉使用例項

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

Plain Text
 
 
 
 
1
public class EnumExample {
2
    public static void main(String[] args) {
3
        Direction north = Direction.NORTH;
4
        System.out.println(north);        //Prints NORTH
5
    }
6
}
7
 
 

列舉的ordinal()方法

ordinal()方法用於獲取列舉變數在列舉類中宣告的順序,下標從0開始,與陣列中的下標很相似。它的設計是用於EumSet和EnumMap複雜的基於列舉的資料結構使用。

Plain Text
 
 
 
 
1
Direction.EAST.ordinal();     //0
2
 
3
Direction.NORTH.ordinal();    //2
4
 
 

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

列舉的values()和valueOf()

values()方法可獲取列舉類中的所有變數,並作為陣列返回:

Plain Text
 
 
 
 
1
Direction[] directions = Direction.values();
2
 
3
for (Direction d : directions) {
4
    System.out.println(d);
5
}
6
 
7
//Output:
8
 
9
EAST
10
WEST
11
NORTH
12
SOUTH
13
 
 

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

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

Plain Text
 
 
 
 
1
Direction east = Direction.valueOf("EAST");
2
         
3
System.out.println(east);
4
 
5
//Output:
6
 
7
EAST
8
 
 

列舉命名約定

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

列舉的構造方法

預設情況下,列舉類是不需要構造方法的,預設的變數就是宣告時的字串。當然,你也可以通過自定義構造方法,來初始化列舉的一些狀態資訊。通常情況下,我們會在構造引數中傳入兩個引數,比如,一個編碼,一個描述。

以上面的方向為例:

Plain Text
 
 
 
 
1
public enum Direction {
2
    // enum fields
3
    EAST(0), WEST(180), NORTH(90), SOUTH(270);
4
 
5
    // constructor
6
    private Direction(final int angle) {
7
        this.angle = angle;
8
    }
9
 
10
    // internal state
11
    private int angle;
12
 
13
    public int getAngle() {
14
        return angle;
15
    }
16
}
17
 
 

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

Plain Text
 
 
 
 
1
Direction north = Direction.NORTH;
2
         
3
System.out.println(north);                      //NORTH
4
 
5
System.out.println(north.getAngle());           //90
6
 
7
System.out.println(Direction.NORTH.getAngle()); //90
8
 
 

列舉中的方法

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

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

Plain Text
 
 
 
 
1
public enum Direction {
2
    // enum fields
3
    EAST, WEST, NORTH, SOUTH;
4
     
5
    protected String printDirection() {
6
        String message = "You are moving in " + this + " direction";
7
        System.out.println( message );
8
        return message;
9
    }
10
}
11
 
 

對應方法的使用如下:

Plain Text
 
 
 
 
1
Direction.NORTH.printDirection(); 
2
Direction.EAST.printDirection(); 
3
 
 

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

Plain Text
 
 
 
 
1
public enum Direction 
2
{
3
    // enum fields
4
    EAST {
5
        @Override
6
        public String printDirection() {
7
            String message = "You are moving in east. You will face sun in morning time.";
8
            return message;
9
        }
10
    },
11
    WEST {
12
        @Override
13
        public String printDirection() {
14
            String message = "You are moving in west. You will face sun in evening time.";
15
            return message;
16
        }
17
    },
18
    NORTH {
19
        @Override
20
        public String printDirection() {
21
            String message = "You are moving in north. You will face head in daytime.";
22
            return message;
23
        }
24
    },
25
    SOUTH {
26
        @Override
27
        public String printDirection() {
28
            String message = "You are moving in south. Sea ahead.";
29
            return message;
30
        }
31
    };
32
 
33
    public abstract String printDirection();
34
}
35
 
 

抽象方法的呼叫,與普通方法一樣:

Plain Text
 
 
 
 
1
Direction.NORTH.printDirection(); 
2
Direction.EAST.printDirection(); 
3
 
 

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

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

Plain Text
 
 
 
 
1
//無法通過編譯,Direction.NORTH是個例項物件
2
 public void text(Direction.NORTH instance){ }
3
 
 

列舉的繼承

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

Plain Text
 
 
 
 
1
public abstract class Enum<E extends Enum<E>>
2
        implements Comparable<E>, Serializable {
3
    // ...
4
}
5
 
 

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

列舉的比較

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

Plain Text
 
 
 
 
1
Direction east = Direction.EAST;
2
Direction eastNew = Direction.valueOf("EAST");
3
 
4
System.out.println( east == eastNew );           //true
5
System.out.println( east.equals( eastNew ) );    //true
6
 
 

列舉集合:EnumSet和EnumMap

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

EnumSet

EnumSet類的定義如下:

Plain Text
 
 
 
 
1
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
2
    implements Cloneable, java.io.Serializable{
3
    // ...
4
}
5
 
 

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

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

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

使用例項:

Plain Text
 
 
 
 
1
public class Test {
2
   public static void main(String[] args) {
3
     Set enumSet = EnumSet.of(  Direction.EAST,
4
                                Direction.WEST,
5
                                Direction.NORTH,
6
                                Direction.SOUTH
7
                              );
8
   }
9
 }
10
 
 

EnumMap

EnumMap的宣告如下:

Plain Text
 
 
 
 
1
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
2
    implements java.io.Serializable, Cloneable
3
{}
4
 
 

與EnumSet類似,EnumMap是一個特殊的Map,Map的Key必須是列舉型別。EnumMap內部是通過陣列實現的,效率比普通的Map更高一些。EnumMap的key值不能為null,並且EnumMap也不是執行緒安全的。

EnumMap使用例項如下:

Plain Text
 
 
 
 
1
public class Test {
2
  public static void main(String[] args){
3
    //Keys can be only of type Direction
4
    Map enumMap = new EnumMap(Direction.class);
5
 
6
    //Populate the Map
7
    enumMap.put(Direction.EAST, Direction.EAST.getAngle());
8
    enumMap.put(Direction.WEST, Direction.WEST.getAngle());
9
    enumMap.put(Direction.NORTH, Direction.NORTH.getAngle());
10
    enumMap.put(Direction.SOUTH, Direction.SOUTH.getAngle());
11
  }
12
}
13
 
 

列舉與switch

使用switch進行條件判斷時,條件引數一般只能是整型,字元型,同時也支援列舉型,在java7後switch也對字串進行了支援。

使用例項如下:

Plain Text
 
 
 
 
1
enum Color {GREEN,RED,BLUE}
2
3
public class EnumDemo4 {
4
5
    public static void printName(Color color){
6
        switch (color){
7
            //無需使用Color進行引用
8
            case BLUE: 
9
                System.out.println("藍色");
10
                break;
11
            case RED:
12
                System.out.println("紅色");
13
                break;
14
            case GREEN:
15
                System.out.println("綠色");
16
                break;
17
        }
18
    }
19
20
    public static void main(String[] args){
21
        printName(Color.BLUE);
22
        printName(Color.RED);
23
        printName(Color.GREEN);
24
    }
25
}
26
 
 

列舉與單例

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

餓漢模式的實現:

Plain Text
 
 
 
 
1
public class Singleton {
2
3
    private static Singleton instance = new Singleton();
4
5
    private Singleton() {
6
    }
7
8
    public static Singleton getInstance() {
9
        return instance;
10
    }
11
}
12
 
 

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

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

Plain Text
 
 
 
 
1
public enum Singleton {
2
3
    INSTANCE;
4
5
    public void doSomething() {
6
        System.out.println("doSomething");
7
    }
8
}
9
 
 

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

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

小結

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

原文連結:http://mp.weixin.qq.com/s/mAhiQcBOCKT9MnT6sNS3BQ

如果覺得本文對你有幫助,可以關注一下我公眾號,回覆關鍵字【面試】即可得到一份Java核心知識點整理與一份面試大禮包!另有更多技術乾貨文章以及相關資料共享,大家一起學習進步!

 

 

「其他文章」