Java枚举类
在某些情况下,一个类的对象是有限而且固定的,比如季节类,它只有4个对象;再比如行星类,目前只有8个对象。这种实例有限而且固定的类,在Java里被称为枚举类。
1.* 手动实现枚举类
如果需要手动实现枚举类,可以采用如下设计方式:
- 通过private将构造器隐藏起来。
- 把这个类的所有可能实例都使用public static final修饰的类变量来保存。
- 如果有必要,可以提供一些静态方法,允许其他程序根据特定参数来获取与之匹配的实例。
下面程序将定义一个Season类,这个类只能产生4个对象,该Season类被定义成一个枚举类:
上面的Season类是一个不可变类,在上面的Season类中包含了4个static final常量Field,这4个常量Field就代表了该类所能创建的对象。当其他程序需要使用Season对象时,既可通过如Season.SPRING的方式来取得Season对象,也可通过getSeason()静态工厂方法来获得Season对象。
下面程序示范了如何使用Season类:
从上面程序中不难看出,使用枚举类可以使程序更加健壮,避免创建对象的随意性。
在更早以前,程序员喜欢使用简单的静态常量来表示这种情况:
点击查看代码
public static final int SEASON_SPRING = 1;
public static final int SEASON_SUMMER = 2;
public static final int SEASON_FALL = 3;
public static final int SEASON_WINTER = 4;
这种定义方法简单明了,但存在如下几个问题:
- 类型不安全:因为上面的每个季节实际上是一个int整数,因此完全可以把一个季节当成一个int整数使用,例如进行加法运算SEASON_SPRING+SEASON_SUMMER,这样的代码完全正常。
- 没有命名空间:当需要使用季节时,必须在SPRING前使用SEASON_前缀,否则程序可能与其他类中的静态常量混淆。
- 打印输出的意义不明确:当我们打印输出某个季节时,例如打印SEASON_SPRING,实际上输出的是1,这个1很难猜测它代表了春天。
从这个意义上来看,枚举类的存在确实很有意义,但手动定义枚举类的代码量比较大,实现起来也比较麻烦,所以Java从JDK 1.5后就增加了对枚举类的支持。
1.* 枚举类入门
Java 5新增了一个enum关键字(它与class、interface关键字的地位相同),用以定义枚举类。正如前面看到的,枚举类是一种特殊的类,它一样可以有自己的Field、方法,可以实现一个或者多个接口,也可以定义自己的构造器。一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同。
但枚举类终究不是普通类,它与普通类有如下简单区别:
- 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是继承Object类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口。
- 使用enum定义、非抽象的枚举类默认会使用final修饰,因此枚举类不能派生子类。
- 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。
- 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加。
所有的枚举类都提供了一个values方法,该方法可以很方便地遍历所有的枚举值。
下面程序定义了一个SeasonEnum枚举类:
编译上面Java程序,将生成一个SeasonEnum.class文件,这表明枚举类是一个特殊的Java类。由此可见,enum关键字和class、interface关键字的作用大致相似。
定义枚举类时,需要显式列出所有的枚举值,如上面的"SPRING,SUMMER,FALL,WINTER"所示,所有的枚举值之间以英文逗号,
隔开,枚举值列举结束后以英文分号作为结束。这些枚举值代表了该枚举类的所有可能的实例。
如果需要使用该枚举类的某个实例,则可使用EnumClass.variable的形式,如SeasonEnum.SPRING:
上面程序测试了SeasonEnum枚举类的用法,该类通过values方法返回了SeasonEnum枚举类的所有实例,并通过循环迭代输出了SeasonEnum枚举类的所有实例。
不仅如此,上面程序的switich表达式中还使用了SeasonEnum对象作为表达式,这是JDK 1.5增加枚举后对switch的扩展:switch的控制表达式可以是任何枚举类型。不仅如此,当switch控制表达式使用枚举类型时,后面case表达式中的值直接使用枚举值的名字,无须添加枚举类作为限定。
前面已经介绍过,所有的枚举类都继承了java.lang.Enum类,所以枚举类可以直接使用java.lang.Enum类中所包含的方法。java.lang.Enum类中提供了如下几个方法:
- int compareTo(E o):该方法用于与指定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数,否则返回零。
- String name():返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与此方法相比,大多数程序员应该优先考虑使用toString()方法,因为toString()方法返回更加用户友好的名称。
- int ordinal():返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为零)。
- String toString():返回枚举常量的名称,与name方法相似,但toString()方法更常用。
- public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name):这是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。
正如前面看到的,当我们使用System.out.println(s)语句来打印枚举值时,实际上输出的是该枚举值的toString()方法,也就是输出该枚举值的名字。
1.* 枚举类的Field、方法和构造器
枚举类也是一种类,只是它是一种比较特殊的类,因此它一样可以定义Field、方法。
下面程序将定义一个Gender枚举类,该枚举类里包含了一个name实例变量:
上面的Gender枚举类里定义了一个名为name的实例变量,并且将它定义成一个public访问权限的。
下面通过如下程序来使用该枚举类:
上面程序使用Gender枚举类时与使用一个普通类没有太大的差别,差别只是产生Gender对象的方式不同,枚举类的实例只能是枚举值,而不是随意地通过new来创建枚举类对象。
正如前面提到的,Java应该把所有类设计成良好封装的类,所以不应该允许直接访问Gender类的name成员变量,而是应该通过方法来控制对name的访问。否则可能出现很混乱的情形,例如上面程序,恰好我们设置了g.name="女",要是采用g.name="男",那程序就会非常混乱了,可能出现FEMALE代表男的局面。为此我们改进了Gender类的设计。
如下面程序所示:
上面程序把name设置成private,从而避免其他程序直接访问该name成员变量,必须通过setName()方法来修改Gender实例的name变量,而setName()方法就可以保证不会产生混乱。
上面程序中setName(String name)部分保证FEMALE枚举值的name变量只能设置为"女",而MALE枚举值的name变量则只能设置为"男"。
下面的测试程序说明了这点:
上面代码中g.setName("男")试图将一个FEMALE枚举值的name变量设置为"男",系统将会提示参数错误。
实际上这种做法依然不够好,枚举类通常应该设计成不可变类,也就是说,它的Field值不应该允许改变,这样会更安全,而且代码更加简洁。为此,我们应该将枚举类的Field都使用privatefinal修饰。
因为我们将所有的Field都使用了final修饰符来修饰,所以必须在构造器里为这些Field指定初始值(或者在定义Field时指定默认值,或者在初始化块中指定初始值,但这两种情况并不常见),因此应该为枚举类显式定义带参数的构造器。
一旦为枚举类显式定义了带参数的构造器,列出枚举值时就必须对应地传入参数:
从上面程序中可以看出,当为Gender枚举类创建了一个Gender(String name)构造器之后,列出枚举值就应该采用[1]处的代码来完成。也就是说,在枚举类中列出枚举值时,实际上就是调用构造器创建枚举类对象,只是这里无须使用new关键字,也无须显式调用构造器。前面列出枚举值时无须传入参数,甚至无须使用括号,仅仅是因为前面的枚举类包含无参数的构造器。
不难看出,上面程序中“MALE("男"), FEMALE("女")”实际上等同于如下两行代码:
点击查看代码
public static final Gender MALE = new Gender("男");
public static final Gender FEMALE = new Gender("女");
1.* 实现接口的枚举类
枚举类也可以实现一个或多个接口。与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口时,也需要实现该接口所包含的方法。
下面程序定义了一个GenderDesc接口:
在上面GenderDesc接口中定义了一个info方法,下面的Gender枚举类实现了该接口,并实现了该接口里包含的info方法。下面是Gender枚举类的代码:
枚举类实现接口不过如此,与普通类实现接口完全一样:使用implements实现接口,实现接口里包含的抽象方法。
如果由枚举类来实现接口里的方法,则每个枚举值在调用该方法时都有相同的行为方式(因为方法体完全一样)。如果需要每个枚举值在调用该方法时呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法,每个枚举值提供不同的实现方式,从而让不同的枚举值调用该方法时具有不同的行为方式。在下面的Gender枚举类中,不同的枚举值对info方法的实现各不相同:
上面代码的[1]处部分看起来有些奇怪:当我们创建MALE和FEMALE两个枚举值时,后面又紧跟了一对花括号,这对花括号里包含了一个info方法定义。如果读者还记得匿名内部类语法的话,则可能对这样的语法有点印象了,花括号部分实际上就是一个类体部分,在这种情况下,当创建MALE、FEMALE枚举值时,并不是直接创建Gender枚举类的实例,而是相当于创建Gender的匿名子类的实例。因为花括号部分实际上是一个匿名内部类的类体部分,所以这个部分的代码语法与前面介绍的匿名内部类语法大致相似,只是它依然是枚举类的匿名内部子类。
此时可能产生疑问,枚举类不是用final修饰了吗?怎么还能派生子类呢?
但是实际上,并不是所有的枚举类都使用了final修饰!非抽象的枚举类才默认使用final修饰。对于一个抽象的枚举类而言——只要它包含了抽象方法,它就是抽象枚举类,系统会默认使用abstract修饰,而不是使用final修饰。
编译上面的程序,可以看到生成了三个文件:
- Gender.class
- Gender$1.class
- Gender$2.class
这样的三个class文件正好证明了上面的结论:MALE和FEMALE实际上是Gender匿名子类的实例,而不是Gender类的实例。这样,当我们调用MALE和FEMALE两个枚举值的方法时,就会看到两个枚举值的方法表现不同的行为方式。
1.* 包含抽象方法的枚举类
假设有一个Operation枚举类,它的4个枚举值“PLUS, MINUS, TIMES, DIVIDE”分别代表加、减、乘、除4种运算。为此,我们定义下面的Operation枚举类:
这里枚举类基本可以运行良好,上面枚举类里的main方法分别使用了PLUS,MINUS,TIMES,DIVIDE4个枚举值进行加、减、乘、除运算,程序运行完全正常。
但我们看上面代码中default: return 0代码行,这行代码完全没有存在的必要:因为this代表了Operation枚举类的一个值,这个值只可能是PLUS,MINUS,TIMES,DIVIDE4个值,根本没有其他的值,但我们又不能不写default: return 0代码,否则无法编译通过。
仔细观察上面的Operation类不难发现,实际上PLUS,MINUS,TIMES,DIVIDE4个值对eval方法各有不同的实现。
为此,我们可以采用前面介绍的方法,让它们分别为4个枚举值提供eval的实现,然后在Operation类中定义一个eval的抽象方法:
编译上面程序会生成5个class文件,其实Operation2对应一个class文件,它的4个匿名内部子类分别各对应一个class文件。
枚举类里定义抽象方法时不能使用abstract关键字将枚举类定义成抽象类(因为系统自动会为它添加abstract关键字),但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。
标签:Java,name,Gender,代码,枚举,实例,方法 From: https://www.cnblogs.com/hzhiping/p/16875173.html