枚举
1. 特点(约束)
Java5 新增了 enum 关键字(它与 class、interface 关键字的地位相同),用于定义枚举类。
枚举类虽然是一种特殊的类,但它一样可以有自己的成员变量、方法,也可以定义自己的构造函器。
一个 Java 源文件中最多只能定义一个 public 修饰的枚举类,且文件名必须和 public 修饰的类名相同。
枚举类和普通类的区别:
- 使用 enum 定义的枚举类默认继承了 java.lang.Enum 类,而不是继承 Object 类,因此枚举类不能显式继承其他任何类。
- java.lang.Enum 类实现了 java.lang.Serializable 和 java.lang.Comparable 接口,因此定义的枚举类也默认实现了上述两个接口。
- 枚举类可以实现一个或多个接口。
- 使用 enum 定义的非抽象的枚举类默认会使用 final 修饰,因此枚举类不能派生子类。
- 枚举类的构造器只能使用 private 修饰,如果省略了构造器的访问控制符,则默认使用 private 修饰。
- 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。
- 系统会对枚举类的实例默认添加 public static final 修饰,无需程序员显式声明。
2. 定义
简单定义:
定义枚举类时,需要显式列出所有的枚举值,值与值之间使用英文逗号(,)隔开,枚举值列举结束后以英文分号(;)结束。
这些枚举值代表了该枚举类所有可能的实例。如下:
public enum Season {
SPRING,SUMMER,FALL,WINTER;
}
如果需要使用该枚举类的某个实例,则可使用 EnumClass.variable 的形式,例如: Season.SPRING
public class EnumTest { public void judge(Season season) { /* * 此处直接使用枚举对象作为表达式,这是 JDK1.5 增加枚举后对 switch 的扩展。 * switch 的控制表达式可以是任何枚举类型,case 表达式中的值可以直接使用枚举值的名字,无须添加枚举类作为限定。 * */ switch (season) { case SPRING: System.out.println("春风佛面来,百花齐开放。"); break; case SUMMER: System.out.println("夏日知鸟哥,孩童水中戏。"); break; case FALL: System.out.println("秋风送粮到,万家笑开颜。"); break; case WINTER: System.out.println("万里银装饰,来年在丰收。"); break; } } public static void main(String[] args) { for (Season s : Season.values()) { // 枚举类默认都有一个 values() 方法,返回该枚举类的所有实例 System.out.println(s); // 实际上输出的是该枚举值的 toString() 方法的值 } new EnumTest().judge(Season.SPRING); } /* * SPRING * SUMMER * FALL * WINTER * 春风佛面来,百花齐开放。 * */ }
Enum 类中的方法:
所有的枚举类都默认继承了 java.lang.Enum 类,所以枚举类可以直接使用 Enum 类中的方法,Enum 类中提供了如下方法:
int compareTo(E o): 比较该枚举对象与指定枚举对象的顺序,同一枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数;否则返回零。
boolean equals(Object other): 当指定对象与该枚举值相等时,返回true。
Class<E> getDeclaringClass(): 返回该枚举值的枚举类型相对应的 Class 对象。
int hashCode(): 返回该枚举值的哈希码。
String name(): 返回该枚举对象的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与此方法相比,通常应优先考虑使用 toString() 方法,因为 toString() 方法返回对用户更加友好的名称。
int ordinal(): 返回该枚举值在枚举类中的索引值(就是枚举值在类中声明的位置,第一个枚举值的索引值为零)。
String toString(): 返回该枚举值的名称,与 name() 方法相似,但 toString() 方法更长用。
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name): 返回指定枚举类中指定名称的枚举值。名称必须与在指定枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。
枚举类中的成员变量、方法和构造器:
成员变量:
枚举类虽然是一种特殊类,但它也可以定义成员变量、方法和构造器。例如下面的 Gender 枚举类中,定义了一个 name 实例变量:
public enum Gender { MALE,FEMALE; // 定义一个实例变量 name public String name; }
上面 Gender 枚举类中定义了一个 name 实例变量,并且将其定义为 public 访问权限。下面通过如下程序来使用该枚举类:
public class GenderTest { public static void main(String[] args) { // 通过 Enum 的 valueOf() 方法来获取指定枚举类的指定枚举值 // Gender female = Enum.valueOf(Gender.class, "FEMALE"); Gender female = Gender.FEMALE; // 直接为枚举值的 name 实例变量赋值 female.name = "女"; // 直接访问该枚举值的 name 实例变量 System.out.println(female + " 代表: " + female.name); // FEMALE 代表: 女 } }
使用枚举类与使用普通类没有太大的差别,差别只是在产生枚举对象的方式不同,枚举类的对象只能是枚举值,而不能通过 new 来创建枚举类对象。
成员方法:
java应该把所有类设计成良好封装的类,所以不应该允许直接访问 Gender 类的 name 成员变量,而应该通过方法来控制对 name 的访问,否则可能出现混乱的情形。
例如上面的程序恰好设置了 female = “女”,如果采用 female = “男”,那么就会出现 FEMALE 代表男的情况,程序逻辑就混乱了。我们可以使用如下代码来改进 Gender 类的设计:
public enum Gender { MALE,FEMALE; // 定义一个实例变量 name,声明为 private,外部不能直接访问 private String name; // 定义访问 name 变量的方法 public void setName(String name) throws Exception { switch(this){ case MALE: if ("男".equals(name)) { this.name = name; }else { throw new Exception("参数错误,需要的参数为: 男,但传入的参数为:" + name); } break; case FEMALE: if ("女".equals(name)) { this.name = name; }else { throw new Exception("参数错误,需要的参数为:女,但传入的参数为:" + name); } break; } } public String getName() { return this.name; } }
public class GenderTest { public static void main(String[] args) throws Exception { // 通过 Enum 的 valueOf() 方法来获取指定枚举类的指定枚举值 // Gender female = Enum.valueOf(Gender.class, "FEMALE"); Gender female = Gender.FEMALE; // 直接为枚举值的 name 实例变量赋值 female.setName("男"); // 直接访问该枚举值的 name 实例变量 System.out.println(female + " 代表: " + female.getName()); /* * Exception in thread "main" java.lang.Exception: 参数错误,需要的参数为:女,但传入的参数为:男 * at demo1.Gender.setName(Gender.java:23) * at demo1.GenderTest.main(GenderTest.java:9) * */ } }
上面程序把 name 设置成 private ,从而避免其他程序直接访问 name 成员变量,必须通过 setName() 方法来设置 name 变量的值,这样就保证了 name 变量的值和对应的对象不会产生混乱。
构造器:
实际上上面的做法依然不够好,枚举类通常应该设计成不可变的类,也就是它的成员变量的值不应该允许改变,这样会更安全,而且代码也更加简洁。
因此建议将枚举类的成员变量都使用 private final 修饰。
如果将所有成员变量都使用 private final 修饰,则必须在构造器里为这些成员变量指定初始值(或者在定义成员变量时指定默认值,或者在初始化块中指定初始值,但这两种方式并不常用)。
一旦为枚举类显式定义了带参数的构造器,列举枚举值时就必须对应的传入参数。例如:
public enum Gender { // 此处的枚举值必须调用对应的构造器来创建 MALE("男"), FEMALE("女"); // 使用 private final 修饰成员变量 private final String name; // 枚举类的构造器默认使用 private 修饰 Gender(String name) { this.name = name; } public String getName() { return this.name; } }
从上面的程序中可以看出,当为枚举类创建了一个构造器后,列出枚举值就应该采用和构造器相对应的方式来完成。
也就是说,在枚举类中列举枚举值时,实际上就是调用相应的构造器来创建枚举类对象,只是这里无须使用 new ,也无须显式调用构造器。
前面列出枚举值时无须传入参数,设置无须使用括号,是因为前面的枚举类中包含无参数的构造器。
实现接口的枚举类:
由枚举类实现接口方法:
枚举类也可以实现一个或多个接口。与普通类实现接口完全一样。例如:
public interface GenderDesc { void info(); }
public enum Gender implements GenderDesc{ // 此处的枚举值必须调用对应的构造器来创建 MALE("男"), FEMALE("女"); // 使用 private final 修饰成员变量 private final String name; // 枚举类的构造器默认使用 private 修饰 Gender(String name) { this.name = name; } public String getName() { return this.name; } @Override public void info() { System.out.println(name); } }
public class GenderTest { public static void main(String[] args) { Gender female = Gender.FEMALE; female.info(); // 女 Gender male = Gender.MALE; male.info(); // 男 } }
由枚举值实现接口方法:
从上面我们可以看出,如果由枚举类来实现接口方法,则每个枚举值在调用该方法时都有相同的行为方式(因为方法体完全一样)。
如果需要每个枚举值在调用该方法时呈现出不同的行为方式,则可以让每个枚举值分别来实现接口方法。
每个枚举值提供不同的实现方式,从而让不同的枚举值调用接口方法时具有不同的行为方式。例如:
public enum Gender implements GenderDesc{ // 此处的枚举值必须调用对应的构造器来创建 MALE("男") { @Override public void info() { System.out.println("这是男性的信息,信息为:" + getName()); } }, FEMALE("女") { @Override public void info() { System.out.println("这是女性的信息,信息为:" + getName()); } }; // 使用 private final 修饰成员变量 private final String name; // 枚举类的构造器默认使用 private 修饰 Gender(String name) { this.name = name; } public String getName() { return this.name; } }
public class GenderTest { public static void main(String[] args) { Gender female = Gender.FEMALE; female.info(); // 这是女性的信息,信息为:女 Gender male = Gender.MALE; male.info(); // 这是男性的信息,信息为:男 } }
从上面的代码中可以看出,当创建 MALE 和 FEMALE 两个枚举值时,后面有紧跟了一对花括号,这对括号里包含了一个 info() 方法定义。
花括号部分实际上就是一个类体部分,这个匿名内部类的语法类似。
在这种情况下,当创建 MALE 和 FEMALE 枚举值时,并不是直接创建 Gender 枚举类的实例,而是相当于创建 Gender 的匿名子类的实例。
前面说过,非抽象的枚举类默认使用 final 修饰,不能派生子类。但并不是所有的枚举类都使用 final 修饰。
对于一个抽象的枚举类而言(只要包含了抽象方法,就是抽象枚举类),系统会默认使用 abstract 修饰。
包含抽象方法的枚举类:
假设有一个 Operation 枚举类,它的4个枚举值 PLUS、MINUS、TIMES、DIVIDE 分别代表加、减、乘、除4中运算,该枚举类需要定义一个 eval() 方法来完成计算。
从上面的描述可以看出,Operation 需要让 PLUS、MINUS、TIMES、DIVIDE 4个值对 eval() 方法各有不同的实现。此时可以考虑为 Operation 枚举类定义一个 eval() 抽象方法,然后让4个值分别提供不同的实现。例如:
public enum Operation { PLUS { @Override public double eval(double x, double y) { return x + y; } }, MINUS { @Override public double eval(double x, double y) { return x - y; } }, TIMES { @Override public double eval(double x, double y) { return x * y; } }, DIVIDE { @Override public double eval(double x, double y) { return x / y; } }; public abstract double eval(double x, double y); public static void main(String[] args) { System.out.println(Operation.PLUS.eval(3, 4)); System.out.println(Operation.MINUS.eval(3, 4)); System.out.println(Operation.TIMES.eval(3, 4)); System.out.println(Operation.DIVIDE.eval(3, 4)); /* * 7.0 * -1.0 * 12.0 * 0.75 * */ } }
枚举类里定义抽象方法时不能使用 abstract 关键字将枚举类定义成抽象类(系统会自动为类添加 abstract 关键字),
但枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。
参考自《疯狂java讲义(第3版)》,将其中一些重要的概念和内容记录下来,以便学习。