程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 計算機程序的思維邏輯 (23),思維23

計算機程序的思維邏輯 (23),思維23

編輯:JAVA綜合教程

計算機程序的思維邏輯 (23),思維23


前面系列,我們介紹了Java中表示和操作數據的基本數據類型、類和接口,本節探討Java中的枚舉類型。

所謂枚舉,是一種特殊的數據,它的取值是有限的,可以枚舉出來的,比如說一年就是有四季、一周有七天,雖然使用類也可以處理這種數據,但枚舉類型更為簡潔、安全和方便。

下面我們就來介紹枚舉的使用,同時介紹其實現原理。

基礎

基本用法

定義和使用基本的枚舉是比較簡單的,我們來看個例子,為表示衣服的尺寸,我們定義一個枚舉類型Size,包括三個尺寸,小/中/大,代碼如下:

public enum Size {
    SMALL, MEDIUM, LARGE
}

枚舉使用enum這個關鍵字來定義,Size包括三個值,分別表示小、中、大,值一般是大寫的字母,多個值之間以逗號分隔。枚舉類型可以定義為一個單獨的文件,也可以定義在其他類內部。

可以這樣使用Size:

Size size = Size.MEDIUM

Size size聲明了一個變量size,它的類型是Size,size=Size.MEDIUM將枚舉值MEDIUM賦值給size變量。

枚舉變量的toString方法返回其字面值,所有枚舉類型也都有一個name()方法,返回值與toString()一樣,例如:

Size size = Size.SMALL;
System.out.println(size.toString());
System.out.println(size.name());

輸出都是SMALL。

枚舉變量可以使用equals和==進行比較,結果是一樣的,例如:

Size size = Size.SMALL;
System.out.println(size==Size.SMALL);
System.out.println(size.equals(Size.SMALL));
System.out.println(size==Size.MEDIUM);

上面代碼的輸出結果為三行,分別是true, true, false。

枚舉值是有順序的,可以比較大小。枚舉類型都有一個方法int ordinal(),表示枚舉值在聲明時的順序,從0開始,例如,如下代碼輸出為1:

Size size = Size.MEDIUM;
System.out.println(size.ordinal());

另外,枚舉類型都實現了Java API中的Comparable接口,都可以通過方法compareTo與其他枚舉值進行比較,比較其實就是比較ordinal的大小,例如,如下代碼輸出為-1,表示SMALL小於MEDIUM:

Size size = Size.SMALL;
System.out.println(size.compareTo(Size.MEDIUM));

枚舉變量可以用於和其他類型變量一樣的地方,如方法參數、類變量、實例變量等,枚舉還可以用於switch語句,代碼如下所示:

static void onChosen(Size size){
    switch(size){
    case SMALL:
        System.out.println("chosen small"); break;
    case MEDIUM:
        System.out.println("chosen medium"); break;
    case LARGE:
        System.out.println("chosen large"); break;
    }
}

在switch語句內部,枚舉值不能帶枚舉類型前綴,例如,直接使用SMALL,不能使用Size.SMALL。

枚舉類型都有一個靜態的valueOf(String)方法,可以返回字符串對應的枚舉值,例如,以下代碼輸出為true:

System.out.println(Size.SMALL==Size.valueOf("SMALL"));

枚舉類型也都有一個靜態的values方法,返回一個包括所有枚舉值的數組,順序與聲明時的順序一致,例如:

for(Size size : Size.values()){
    System.out.println(size);
}

屏幕輸出為三行,分別是SMALL, MEDIUM, LARGE。

枚舉的好處

Java是從JDK 5才開始支持枚舉的,在此之前,一般是在類中定義靜態整形變量來實現類似功能,代碼如下所示:

class Size {
    public static final int SMALL = 0;
    public static final int MEDIUM = 1;
    public static final int LARGE = 2;
}

枚舉的好處是比較明顯的:

  • 定義枚舉的語法更為簡潔。
  • 枚舉更為安全,一個枚舉類型的變量,它的值要麼為null,要麼為枚舉值之一,不可能為其他值,但使用整形變量,它的值就沒有辦法強制,值可能就是無效的。
  • 枚舉類型自帶很多便利方法(如values, valueOf, toString等),易於使用。 

基本實現原理

枚舉類型實際上會被Java編譯器轉換為一個對應的類,這個類繼承了Java API中的java.lang.Enum類。

Enum類有兩個實例變量name和ordinal,在構造方法中需要傳遞,name(), toString(), ordinal(), compareTo(), equals()方法都是由Enum類根據其實例變量name和ordinal實現的。

values和valueOf方法是編譯器給每個枚舉類型自動添加的,上面的枚舉類型Size轉換後的普通類的代碼大概如下所示:

public final class Size extends Enum<Size> {
    public static final Size SMALL = new Size("SMALL",0);
    public static final Size MEDIUM = new Size("MEDIUM",1);
    public static final Size LARGE = new Size("LARGE",2);
    
    private static Size[] VALUES =
            new Size[]{SMALL,MEDIUM,LARGE};
    
    private Size(String name, int ordinal){
        super(name, ordinal);
    }
    
    public static Size[] values(){
        Size[] values = new Size[VALUES.length];
        System.arraycopy(VALUES, 0,
                values, 0, VALUES.length);
        return values;
    }
    
    public static Size valueOf(String name){
        return Enum.valueOf(Size.class, name);
    }
}

解釋幾點:

  • Size是final的,不能被繼承,Enum<Size>表示父類,<Size>是泛型寫法,我們後續文章介紹,此處可以忽略。
  • Size有一個私有的構造方法,接受name和ordinal,傳遞給父類,私有表示不能在外部創建新的實例。
  • 三個枚舉值實際上是三個靜態變量,也是final的,不能被修改。
  • values方法是編譯器添加的,內部有一個values數組保持所有枚舉值。
  • valueOf方法調用的是父類的方法,額外傳遞了參數Size.class,表示類的類型信息,類型信息我們後續文章介紹,父類實際上是回過頭來調用values方法,根據name對比得到對應的枚舉值的。 

一般枚舉變量會被轉換為對應的類變量,在switch語句中,枚舉值會被轉換為其對應的ordinal值。

可以看出,枚舉類型本質上也是類,但由於編譯器自動做了很多事情,它的使用也就更為簡潔、安全和方便。

典型場景

用法

以上枚舉用法是最簡單的,實際中枚舉經常會有關聯的實例變量和方法,比如說,上面的Size例子,每個枚舉值可能有關聯的縮寫和中文名稱,可能需要靜態方法根據縮寫返回對應的枚舉值,修改後的Size代碼如下所示:

public enum Size {
    SMALL("S","小號"),
    MEDIUM("M","中號"),
    LARGE("L","大號");
    
    private String abbr;
    private String title;
    
    private Size(String abbr, String title){
        this.abbr = abbr;
        this.title = title;
    }

    public String getAbbr() {
        return abbr;
    }

    public String getTitle() {
        return title;
    }
    
    public static Size fromAbbr(String abbr){
        for(Size size : Size.values()){
            if(size.getAbbr().equals(abbr)){
                return size;
            }
        }
        return null;
    }
}

以上代碼定義了兩個實例變量abbr和title,以及對應的get方法,分別表示縮寫和中文名稱,定義了一個私有構造方法,接受縮寫和中文名稱,每個枚舉值在定義的時候都傳遞了對應的值,同時定義了一個靜態方法fromAbbr根據縮寫返回對應的枚舉值。

需要說明的是,枚舉值的定義需要放在最上面,枚舉值寫完之後,要以分號(;)結尾,然後才能寫其他代碼。

這個枚舉定義的使用與其他類類似,比如說:

Size s = Size.MEDIUM;
System.out.println(s.getAbbr());

s = Size.fromAbbr("L");
System.out.println(s.getTitle());

以上代碼分別輸出: M, 大號

實現原理

加了實例變量和方法後,枚舉轉換後的類與上面的類似,只是增加了對應的變量和方法,修改了構造方法,代碼不同之處大概如下所示:

public final class Size extends Enum<Size> {
  public static final Size SMALL =        
          new Size("SMALL",0, "S", "小號");     
  public static final Size MEDIUM =       
          new Size("MEDIUM",1,"M","中號");      
  public static final Size LARGE =        
          new Size("LARGE",2,"L","大號");       
                                          
  private String abbr;                    
  private String title;                   
                                          
  private Size(String name, int ordinal,  
          String abbr, String title){         
      super(name, ordinal);               
      this.abbr = abbr;                   
      this.title = title;                 
  }
  //... 其他代碼
}  

說明

每個枚舉值經常有一個關聯的標示(id),通常用int整數表示,使用整數可以節約存儲空間,減少網絡傳輸。一個自然的想法是使用枚舉中自帶的ordinal值,但ordinal並不是一個好的選擇。

為什麼呢?因為ordinal的值會隨著枚舉值在定義中的位置變化而變化,但一般來說,我們希望id值和枚舉值的關系保持不變,尤其是表示枚舉值的id已經保存在了很多地方的時候。

比如說,上面的Size例子,Size.SMALL的ordinal的值為0,我們希望0表示的就是Size.SMALL的,但如果我們增加一個表示超小的值XSMALL呢?

public enum Size {
    XSMALL, SMALL, MEDIUM, LARGE
}

這時,0就表示XSMALL了。

所以,一般是增加一個實例變量表示id,使用實例變量的另一個好處是,id可以自己定義。比如說,Size例子可以寫為:

public enum Size {
    XSMALL(10), SMALL(20), MEDIUM(30), LARGE(40);
    
    private int id;
    private Size(int id){
        this.id = id;
    }
    public int getId() {
        return id;
    }
}

高級用法

枚舉還有一些高級用法,比如說,每個枚舉值可以有關聯的類定義體,枚舉類型可以聲明抽象方法,每個枚舉值中可以實現該方法,也可以重寫枚舉類型的其他方法。

比如說,我們看改後的Size代碼(這個代碼實際意義不大,主要展示語法):

public enum Size {
    SMALL {
        @Override
        public void onChosen() {
            System.out.println("chosen small");
        }
    },MEDIUM {
        @Override
        public void onChosen() {
            System.out.println("chosen medium");
        }
    },LARGE {
        @Override
        public void onChosen() {
            System.out.println("chosen large");
        }
    };
    
    public abstract void onChosen();
} 

Size枚舉類型定義了onChosen抽象方法,表示選擇了該尺寸後執行的代碼,每個枚舉值後面都有一個類定義體{},都重寫了onChosen方法。

這種寫法有什麼好處呢?如果每個或部分枚舉值有一些特定的行為,使用這種寫法比較簡潔。對於這個例子,上面我們介紹了其對應的switch語句,在switch語句中根據size的值執行不同的代碼。

switch的缺陷是,定義swich的代碼和定義枚舉類型的代碼可能不在一起,如果新增了枚舉值,應該需要同樣修改switch代碼,但可能會忘記,而如果使用抽象方法,則不可能忘記,在定義枚舉值的同時,編譯器會強迫同時定義相關行為代碼。所以,如果行為代碼和枚舉值是密切相關的,使用以上寫法可以更為簡潔、安全、容易維護。

這種寫法內部是怎麼實現的呢?每個枚舉值都會生成一個類,這個類繼承了枚舉類型對應的類,然後再加上值特定的類定義體代碼,枚舉值會變成這個子類的對象,具體代碼我們就不贅述了。

枚舉還有一些其他高級用法,比如說,枚舉可以實現接口,也可以在接口中定義枚舉,使用相對較少,本文就不介紹了。

小結

本節介紹了枚舉類型,介紹了基礎用法、典型場景及高級用法,不僅介紹了如何使用,還介紹了實現原理,對於枚舉類型的數據,雖然直接使用類也可以處理,但枚舉類型更為簡潔、安全和方便。

我們之前提到過異常,但並未深入討論,讓我們下節來探討。

----------------

未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。

-----------

更多相關原創文章

計算機程序的思維邏輯 (13) - 類

計算機程序的思維邏輯 (14) - 類的組合

計算機程序的思維邏輯 (15) - 初識繼承和多態

計算機程序的思維邏輯 (16) - 繼承的細節

計算機程序的思維邏輯 (17) - 繼承實現的基本原理

計算機程序的思維邏輯 (18) - 為什麼說繼承是把雙刃劍

計算機程序的思維邏輯 (19) - 接口的本質

計算機程序的思維邏輯 (20) - 為什麼要有抽象類?

計算機程序的思維邏輯 (21) - 內部類的本質

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved