最近更新: 2009-12-09

Java Enum and Generic

Enum(列舉) 在 C 語言時代就是賦予常數值可讀意義的簡便方法。 C# 也是一開始就提供 Enum 型別。 Java 則遲到 5.0 才提供。不過遲來總比不來好。

本文是 Java 語言的 Enum (列舉)型別與 Generic (泛型) 能力共同運作的筆記。 Java 的列舉型別是一種特殊型別,當我們要在列舉型別的場合中加上泛型能力時,需要運用一些不同的處理手段。我們也需要用到 Reflection (反射)。

基於 Java 語言的特性, Enum 應用在方法的參數傳遞與回傳值上,具有提高表達能力以及強制內容檢查的兩種好處。

尚未使用 Enum 之前:

public class Main {
    public int where(int type) {
        if (type == 1) {
            //something
        }
        else if (type == 2) {
            //something
        }
        else {
            //3,4,5,... 沒意義的值,錯誤的值。
            return 1; //??
        }
        return 0; //什麼意思?
    }
}

使用 Enum 之後。

public class Main2 {
    public enum WhereType {
        Asc,
        Desc
    }

    public enum ReturnCode {
        Ok,
        Error
    }

    public ReturnCode where(WhereType type) {
        if (type == WhereType.Desc) {
            //something
        }
        else if (type == WhereType.Asc) {
            //something
        }
        //不用再處理 3,4,5 這些錯誤的值。

        return ReturnCode.Ok;
    }
}

在你直接以數值代表參數的某些意義時,你可能要提防方法調用者傳一些預期以外的參數值。你使用 Enum 之後,除了賦予參數值可讀的意義之外,亦強制調用者只能傳你列舉的參數值進來,你不再需要關心非預期的參數值。同理,當你使用 Enum 處理回傳值時,方法調用者也更易懂也能更簡單地處理你的回傳值。

那麼,當 Java 的 Enum 碰上 泛型時,又會發生什麼事?

我先列出3個示範用的列舉定義。

public enum ReturnCode {
	Error,
	Ok
}


public enum HttpStatus {
	Ok,
	Not_Found,
	Error
}

public enum DaoReturnCode {
    Ok,
    Not_Found,
	Error,
    Exists
}

案例一: Enum 作為泛型方法的參數值

這是最簡單的情形。

public class Main {
	void case1(HttpStatus code) {
		if (code == HttpStatus.Ok)
			System.out.println("Ok");
		else
			System.out.println("Not ok");

        switch (code) {
            case Ok:
                System.out.println("Ok");
                break;
            default:
                break;
        }
	}

	<EnumType> void genericCase1(EnumType code) {
		//if (code == EnumType.Ok) //ERROR
		if (code.toString() == "Ok")
			System.out.println("Ok");
		else
			System.out.println("Not ok");

        /*ERROR! Java 的 switch 不接受數值或 Enum 以外的型別
        switch (code.toString()) {
            case "Ok":
                System.out.println("Ok");
                break;
            default:
                break;
        }
        */
	}

	public static void main(String[] args) {
		Main m = new Main();

		m.case1(HttpStatus.Ok);
		m.genericCase1(ReturnCode.Ok);
		m.genericCase1(DaoReturnCode.Ok);
	}
}

case1() 是一般方法,genericCase1() 則是泛型方法。基本上, Enum 作為 Java 的新特殊型別, Java 的泛型並未能提供適切的支援。因此我們要忽略參數 code 是一個 Enum 的事,將它視為一般實體,調用它的 toString() 方法取得它的符號字串,和一般字串比對。這也意味著我們不能用 switch/case 處理列舉項目。

案例二: Enum 作為泛型方法的回傳值

import java.lang.reflect.*;

public class Main {
	HttpStatus case2() {
		return HttpStatus.Ok;
	}

//ERROR.
//	<EnumType> EnumType genericCase1() {
//		return EnumType.Ok;
//	}

	@SuppressWarnings("unchecked")
	<EnumType extends Enum<EnumType>> EnumType genericCase2(Class<EnumType> clazz) {
		return Enum.valueOf(clazz, "Ok");

	}
/* //  Another way:
	<EnumType> EnumType genericCase2(Class<EnumType> clazz) {
		Field f = null;
		try {
			f = clazz.getField("Ok");
		} catch (SecurityException e) {
			// TODO Auto-generated catch block
		} catch (NoSuchFieldException e) {
			// TODO Auto-generated catch block
		}

		EnumType rc = null;
		try {
			rc = (EnumType) f.get(null);
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
		}
		return rc;

	}
*/
	public static void main(String[] args) {
		Main m = new Main();

		if (m.case2() == HttpStatus.Ok) {
			System.out.println("Ok");
		}
		if (m.genericCase2(ReturnCode.class) == ReturnCode.Ok) {
			System.out.println("Ok");
		}
		if (m.genericCase2(DaoReturnCode.class) == DaoReturnCode.Ok) {
			System.out.println("Ok");
		}
	}
}

case2() 是一般方法,genericCase2() 則是泛型方法。我們必須使用 Enum 的方法 valueOf() ,而它需要一個類別作為參數。所以泛型方法需要多一個類別參數 clazz ( 或許你會想用反射機制去看 genericCase2() 的回傳值型態,以免多傳一個參數。但結果將會令你失望。 )。

另外一個方法是用 reflect。列舉值被視為 Enum 類別中的一個欄位(field),所以請用反射方法getField()取得我們需要的列舉值欄位。再用欄位的方法get()得到列舉值。

結論

當我們想要將一個使用了 Enum 的類別或方法泛型化時,其結果要分兩方面來談。對類別或方法的使用者而言,他不會察覺任何不同,仍然保有 Enum 帶來的利益。對泛型類別或泛型方法的設計者而言,Enum 的資訊幾乎消失了, Enum 的利益蕩然無存

在案例一中提到在泛型方法內部,我們只能用 toString() 判斷列舉值,因此不能用 switch/case 處理列舉項目。這僅是問題的冰山一角。另一個更嚴重的問題在於編譯器也失去了列舉符號與定義的繫結,因此編譯器(與 IDE)不再能幫我們檢查列舉定義的變動。

以本文範例說明,如果我修改 HttpStatus 的列舉值名稱,將 HttpStatus.Ok 改成 HttpStatus.OK(全大寫),此時編譯器與 IDE 可以追蹤出一般方法 case1(), case2() 中的列舉值未定義(沒有改名稱)。但是卻沒有辦法幫我們發現泛型方法 genericCase1(), genericCase2() 中的列舉值還沒改,因它已經看不到泛型方法內部的列舉資訊,只看到字串的處理動作。

哪麼我們何時才會發現我們還沒改泛型方法內部的東西呢?假設我們把事情忘得夠乾淨,那麼我們會在執行單元測試時,發現測試結果有失敗項目(如果你沒有寫單元測試的習慣,那麼只有天知道你何時會發現錯誤)。我們以失敗項目為進入點,找出問題出在方法的回傳值不符合期望值。當我們開啟發出問題的泛型方法的源碼文件時,可能還要先喚出版本控制工具的歷史記錄幫助我們的小腦袋回憶我們做了什麼,才會想到泛型方法內的 "Ok" 應該要改成 "OK"

這整件事的理想狀況反應是,當我修改一個列舉的定義後,編譯器(或IDE)可以抓出這個列舉不能作為某個泛型方法的型態參數,或者反過來抓出接受這個列舉作為型態參數的泛型方法的形式不符預期。

例如 HttpStatus, ReturnCode, DaoReturnCode 原先在某個場合都用 genericCase1() 處理一個會判斷 Ok 的動作。那麼當我把 HttpStatus.Ok 改成 HttpStatus.OK 時,理想的狀況反應是編譯器(或IDE)會說:

  1. HttpStatus 不能作為 genericCase1()的型態參數。
  2. genericCase1()不符預期形式。

那麼我們看到編譯器(或IDE)的反應後,就可以選擇至少三種後續手段:

  1. 把 HttpStatus.OK 改回去(不改了)。
  2. 把另外兩個列舉的 Ok 改成 OK.
  3. 寫一個特化方法給 HttpStatus 用。也就是 HttpStatus 改成用 case1() 處理。

然而從上述案例可以明顯地看出 Java 編譯器(或IDE)對這兩種狀況反應都做不到。所以我結論說在 Java 語言的泛型類別或泛型方法內部, Enum 的利益蕩然無存。

在 Java 泛型的不適用案例中,需要多加一筆: 當一個類別或方法使用了 Enum 時,它可能不適合泛型化。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/10960895.html

樂多舊回應
未留名 (#comment-20162117)
Wed, 09 Dec 2009 17:06:44 +0800
昨晚發文時大概累了,竟然漏了一段結論沒貼上。
補上。
未留名 (#comment-20164061)
Thu, 10 Dec 2009 10:27:40 +0800
上面好像少了字,重打
不太知道你的例子主要用意.例如不是所有自定義enum type都會有Ok,所以只靠<EnumType>或是<EnumType extends Enum<EnumType>>都無法去直接跟Ok去比對.外加如果可以, 那麼假設我的程式中刻意Ok跟OK混用,那該檢查出怎樣的結果?

倒是第二個例子可以簡單點,雖然還是得在runtime檢查錯誤.
<T extends Enum<T>> static T genericCase2(Class<T> code) {
return Enum.valueOf(code, "Ok");
}
未留名 (#comment-20164285)
Thu, 10 Dec 2009 11:44:14 +0800
先謝謝koji的提點。
我試太多方法陷入誤區了,原先一直在試不另外傳類別的方法,怎麼試都沒辦法用 Enum.valueOf()。
我都忘了既然我最後還是加了一個類別參數,那麼就可以用 Enum.valueOf() 了。

第二,思考方向不是從泛型方法來看自定Enum是不是都要有某個值。而是從自定的Enum來看。

理想的狀況反應是... [回應內容補充到正文的結論去了]。
未留名 (#comment-20164383)
Thu, 10 Dec 2009 12:18:58 +0800
您好
我大概懂意思了,我是一開始就從Java的觀點看的關係,所以一開始搞不大懂你想要的東西.
未留名 (#comment-20183643)
Tue, 15 Dec 2009 17:21:27 +0800
哇!原來還可以這樣用~
JDK 5 寫太少了~~~
感覺還在 1.4 的階段~~~
未留名 (#comment-21721851)
Fri, 22 Apr 2011 03:52:23 +0800
理想狀況是你要compiler提醒你HttpStatus的變動?
好笑!!
genericCase1方法裡連HttpStatus列舉的H都沒提到
請問一下compiler要根據什麼來提醒?
把這篇聳動的結論改掉吧,沒意義的東西...
未留名 (#comment-21739487)
Wed, 04 May 2011 17:46:29 +0800
我在正文開頭就指出 Enum 在 Java 語言存在的用意「基於 Java 語言的特性, Enum 應用在方法的參數傳遞與回傳值上,具有提高表達能力以及強制內容檢查的兩種好處。」

這句話的意思就是在說,當你對列舉做出變動時,編譯器可以檢查並提醒你哪些地方要跟著改變。

然而當你配合 Generic 後,這些好處不見了。

jfire 完全把 Enum 和 Generic 分割來看,自然覺得他碰到的情形都是「理所當然」,而不會是問題。
未留名 (#comment-21756493)
Sat, 14 May 2011 02:15:49 +0800
寫Enum泛型類別/方法本身就是一件沒有意義、莫名其妙的事情,不會有人這樣做

泛型存在的意義就是作為一個標籤,標記著一個類型

寫就跟寫是差不多,沒什麼用處
未留名 (#comment-21756497)
Sat, 14 May 2011 02:19:15 +0800
補上
寫Enum泛型就跟寫Object泛型
未留名 (#comment-21771489)
Tue, 24 May 2011 16:33:54 +0800
如果可以選擇的話,我當時也不想改這種東西。

不幸的是,我當時的工作就需要將一個原本用了 Enum 的程式碼套上泛型能力。

所以,我寫了這篇文章,以說明這件事沒有任何利益。講白話些,就是 jfire 說的「寫Enum泛型類別/方法本身就是一件沒有意義、莫名其妙的事情」。

或許 jfire 有鐵口直斷的本事,不必要對誰說明前因後果。但我這個人的個性就是要將原因解釋出來,否則就不說「我知道了」。
abc@gmail.com(Anonymous) (#comment-21950975)
Wed, 31 Aug 2011 13:18:27 +0800
enum in java is very versatile than just a enumerated type here is good link of various examples of enum in java