本篇简单介绍Java中的枚举。
在枚举出现以前,一般使用接口常量或者使用public static final修饰的成员变量来定义某个变量的有限的几个常量值,例如学历(小学,初中,高中,大学,研究生,博士)使用接口常量和public static final修饰的成员变量表示如下:
//接口常量
public interface Constants {
Integer PRIMARY_SCHOOL = 1;
Integer JUNIOR_MIDDLE_SCHOOL = 2;
Integer HIGH_SCHOOL = 3;
Integer UNIVERSITY = 4;
Integer POSTGRADUATE = 5;
Integer DOCTOR = 6;
}
//public static final修饰的成员变量
public class AnoConstants {
public static final Integer PRIMARY_SCHOOL = 1;
public static final Integer JUNIOR_MIDDLE_SCHOOL = 2;
public static final Integer HIGH_SCHOOL = 3;
public static final Integer UNIVERSITY = 4;
public static final Integer POSTGRADUATE = 5;
public static final Integer DOCTOR = 6;
}
JDK1.5中出现了枚举类型enum,这种类型可以定义专属于它自己的“属性”和方法,Java也提供了一些方法以方便我们使用枚举,因此在类似以上场景中我们应尽可能地使用枚举。
枚举类型的定义
枚举类型使用enum关键字替代class关键字定义,一般的枚举类型定义如下:
修饰符 enum 枚举名称 {
枚举类型1,
枚举类型2,
...
枚举类型n;
其他方法
}
其中枚举类型的作用便相当于接口常量,枚举类型的列表必须在枚举类的开始就列出,以“,”分隔并以“;”结束。使用枚举类型重新定义学历的几个限定值如下:
public enum EducationEnum {
PRIMARY_SCHOOL(1,"小学"),
JUNIOR_MIDDLE_SCHOOL(2,"中学"),
HIGH_SCHOOL(3,"高中"),
UNIVERSITY(4,"大学"),
POSTGRADUATE(5,"研究生"),
DOCTOR(6,"博士");
private Integer key;
private String value;
EducationEnum(Integer key, String value) {
this.key = key;
this.value = value;
}
public Integer getKey() {
return key;
}
public String getValue() {
return value;
}
}
需要注意的是,EducationEnum类中的成员变量key和value可选择的,即你可以选择不定义任何成员变量、可选择定义1或n个成员变量、可选择定义每个成员变量的名称。枚举类定义与一般java类定义最重要的区别就是enum关键字替代了class关键字,enum关键字不仅说明了当前类是一个枚举类型,它还包括以下作用(在本文末尾会验证下述结论):
- 使当前枚举类隐式继承Enum类,因此枚举类不能继承任何类,但可以实现接口;
- 它具有final语义,即被enum关键字修饰的类不能被继承;
- 其定义的枚举类型可以当作是Enum类的被public static final修饰符修饰的实例,因此枚举类中定义的成员变量必须在定义枚举类型时给出。
枚举类型的使用
上面提到每个enum修饰的枚举类都隐式地继承了Enum类,因此我们首先看Enum类的主要成员变量和成员方法。
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
private final String name;
private final int ordinal;
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public String toString() {
return name;
}
public final int compareTo(E o) {
Enum other = (Enum)o;
Enum self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum const " + enumType +"." + name);
}
protected final void finalize() { }
}
- name:枚举类中定义的枚举类型的名称;
- ordinal:枚举类中定义的枚举类型的顺序;
- toString():Enum重写了toString方法,返回枚举类型名称;
- compareTo():Enum实现了Comparable接口,重写compareTo()方法,该方法比较的是同一个枚举类的枚举类型的ordinal值;
- valueOf():可以看作是toString()方法的逆方法,由枚举类型的名称获得枚举类型实例。
除了以上方法外,编译器还会添加一个values()方法,该方法返回包含全部枚举类型的集合。另外,我们还可以在枚举类中定义成员方法,例如通过EducationEnum的key获取value的方法:
public static String getValueByKey(Integer key) {
for (EducationEnum education:values()) {
if (education.getKey().equals(key)) {
return education.getValue();
}
}
return null;
}
下面的例子说明了以上成员变量以及方法的使用:
public static void main(String[] args) {
for (EducationEnum education:EducationEnum.values()) {
System.out.println("枚举类型名称:" + education.name());
System.out.println("枚举类型定义的顺序:" + education.ordinal());
System.out.println("toString()方法输出:" + education.toString());
System.out.println("枚举类型的key:" + education.getKey() + ",value:" + education.getValue());
System.out.println();
}
System.out.println(EducationEnum.JUNIOR_MIDDLE_SCHOOL.compareTo(EducationEnum.PRIMARY_SCHOOL));
System.out.println(EducationEnum.JUNIOR_MIDDLE_SCHOOL.compareTo(EducationEnum.JUNIOR_MIDDLE_SCHOOL));
System.out.println(EducationEnum.JUNIOR_MIDDLE_SCHOOL.compareTo(EducationEnum.POSTGRADUATE));
System.out.println();
//valueOf()方法通过name得到实例,toString()方法又输出name
System.out.println(EducationEnum.valueOf("POSTGRADUATE"));
System.out.println();
System.out.println(EducationEnum.getValueByKey(5));
System.out.println(EducationEnum.getValueByKey(10));
}
控制台打印输出如下:
枚举类型名称:PRIMARY_SCHOOL
枚举类型定义的顺序:0
toString()方法输出:PRIMARY_SCHOOL
枚举类型的key:1,value:小学
枚举类型名称:JUNIOR_MIDDLE_SCHOOL
枚举类型定义的顺序:1
toString()方法输出:JUNIOR_MIDDLE_SCHOOL
枚举类型的key:2,value:中学
枚举类型名称:HIGH_SCHOOL
枚举类型定义的顺序:2
toString()方法输出:HIGH_SCHOOL
枚举类型的key:3,value:高中
枚举类型名称:UNIVERSITY
枚举类型定义的顺序:3
toString()方法输出:UNIVERSITY
枚举类型的key:4,value:大学
枚举类型名称:POSTGRADUATE
枚举类型定义的顺序:4
toString()方法输出:POSTGRADUATE
枚举类型的key:5,value:研究生
枚举类型名称:DOCTOR
枚举类型定义的顺序:5
toString()方法输出:DOCTOR
枚举类型的key:6,value:博士
1
0
-3
POSTGRADUATE
研究生
null
枚举类也常常使用在switch语句中,例如:
private static final EducationEnum education = EducationEnum.HIGH_SCHOOL;
public static void main(String[] args) {
switch (education) {
case EducationEnum.PRIMARY_SCHOOL:
System.out.println("小学");
break;
case EducationEnum.JUNIOR_MIDDLE_SCHOOL:
System.out.println("中学");
break;
case EducationEnum.HIGH_SCHOOL:
System.out.println("高中");
break;
case EducationEnum.UNIVERSITY:
System.out.println("大学");
break;
case EducationEnum.POSTGRADUATE:
System.out.println("研究生");
break;
case EducationEnum.DOCTOR:
System.out.println("博士");
break;
default:
System.out.println("???????????");
}
}
输出结果:
高中
枚举的好处在这个例子中就体现了,例如数据库中存储的可能是该枚举类型的key值,使用枚举我们在代码中就可以像上面这样不用关心每个枚举值的key值是什么,直接使用枚举实例就可以了。笔者在工作中遇到了老代码中很多不使用枚举或者错误使用枚举的情况,例如:
- 没定义枚举类,性别字段不使用枚举类型而是使用String或Integer,仅在数据库中维护备注(0-男,1-女),导致在代码中次次需要看Bean的备注。另外,扩展性也极差,如果哪天使用1表示男性,2表示女性,每一处使用该字段的代码岂不是都要变动。定义枚举GenderEnum.MALE和GenderEnum.FEMALE就简单的解决了。
- 定义了GenderEnum枚举类,但在DTO中不用该类型修饰枚举字段而是使用String或Integer。这种情况较上面的情况好一些,有了扩展性,但每次判断值时(例如switch语句)都需要再从枚举实例中获取key或value值。
反编译枚举类
为了验证前面提到的结论,最简单的方法是查看该枚举类的反编译文件(首先将该枚举类的.java文件编译为.class文件,然后使用反编译工具将.class文件反编译)。这里多提一句,笔者首先使用的是JD-GUI工具,发现其反编译的文件与初始的.java文件几乎相同。然后笔者又尝试使用了jad反编译工具,得到了想要的结果,因此如果想要查看反编译后的文件建议使用jad。笔者以后会写一篇文章专门介绍Java中流行的反编译工具,因此此处不再赘述反编译过程。
EducationEnum枚举类型反编译后的内容如下:
public final class EducationEnum extends Enum
{
public static EducationEnum[] values()
{
return (EducationEnum[])$VALUES.clone();
}
public static EducationEnum valueOf(String name)
{
return (EducationEnum)Enum.valueOf(com/company/EducationEnum, name);
}
private EducationEnum(String s, int i, Integer key, String value)
{
super(s, i);
this.key = key;
this.value = value;
}
public static final EducationEnum PRIMARY_SCHOOL;
public static final EducationEnum JUNIOR_MIDDLE_SCHOOL;
public static final EducationEnum HIGH_SCHOOL;
public static final EducationEnum UNIVERSITY;
public static final EducationEnum POSTGRADUATE;
public static final EducationEnum DOCTOR;
private Integer key;
private String value;
private static final EducationEnum $VALUES[];
static
{
PRIMARY_SCHOOL = new EducationEnum("PRIMARY_SCHOOL", 0, Integer.valueOf(1), "\u5C0F\u5B66");
JUNIOR_MIDDLE_SCHOOL = new EducationEnum("JUNIOR_MIDDLE_SCHOOL", 1, Integer.valueOf(2), "\u4E2D\u5B66");
HIGH_SCHOOL = new EducationEnum("HIGH_SCHOOL", 2, Integer.valueOf(3), "\u9AD8\u4E2D");
UNIVERSITY = new EducationEnum("UNIVERSITY", 3, Integer.valueOf(4), "\u5927\u5B66");
POSTGRADUATE = new EducationEnum("POSTGRADUATE", 4, Integer.valueOf(5), "\u7814\u7A76\u751F");
DOCTOR = new EducationEnum("DOCTOR", 5, Integer.valueOf(6), "\u535A\u58EB");
$VALUES = (new EducationEnum[] {
PRIMARY_SCHOOL, JUNIOR_MIDDLE_SCHOOL, HIGH_SCHOOL, UNIVERSITY, POSTGRADUATE, DOCTOR
});
}
}
从反编译文件来看,可以得出以下结论:
- enum修饰的枚举类继承了Enum类,因此不能再继承其他类;
- enum修饰的类被final修饰,因此不能被继承;
- 定义的每个枚举类型都是该枚举类的一个public static final修饰的实例,枚举类的成员变量即是枚举类型的成员变量;
- 枚举类型在static块中初始化,因此是线程安全的,这也是枚举类型实现的单例模式是线程安全的原因。
笔者再想到关于枚举的本篇文章没提到的会随时更新。