一个类的对象是有限而且固定的,比如季节类, 它只有4个对象;再比如行星类,目前只有8个对象。这种实例有限而且固定的类,在Java里被称为枚举类。在早期代码中,可能会直接使用简单的静态常量来表示枚,这种定义方法简单明了,但存在如下几个问题。也就是为啥静态常量定义枚举类为啥不好:
1.类型不安全:因为上面的每个季节实际上是一个int整数,因此完全可以把一个季节当成一个int整数使用,例如进行加法运算SEASON_SPRING+SEASON_SUMMER,这样的代码完全正常。
2.没有命名空间:当需要使用季节时,必须在SPRING前使用SEASON_前缀,否则程序可能与其他类中的静态常量混淆。
3.打印输出的意义不明确:当输出某个季节时,例如输出SEASON_SPRING,实际上输出的是1,这个1很难猜测到它代表了春天。
用普通的类实现枚举类也没问题,因此早期也可采用通过定义类的方式来实现,可以采用如下设计方式。
1.通过private将构造器隐藏起来。
2.把这个类的所有可能实例都使用public static final修饰的类变量来保存。
3.如果有必要,可以提供一些静态方法,允许其他程序根据特定参数来获取与之匹配的实例。
使用枚举类可以使程序更加健壮,避免创建对象的随意性但通过定义类来实现枚举的代码量比较大,实现起来也比较麻烦,Java从JDK 1.5后就增加了对枚举类的支持。Java 5新增了一个enum关键字(它与class、interface关键字的地位相同)用以定义枚举类,枚举类是一种特殊的类,它一样可以有自己的成员变量、方法,可以实现一个或者多个接口,也可以定义自己的构造器。一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同。
但枚举类终究不是普通类,它与普通类有如下简单区别。
1.枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是默认继承Object类,因此枚举类不能显式继承其他父类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口。
2.使用enum定义、非抽象的枚举类默认会使用final修饰。
3.枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。由于枚举类的所有构造器都是private的,而子类构造器总要调用父类构造器一次,因此枚举类不能派生子类。如果枚举类有子类,子类需要调用枚举类的构造器,但是枚举类的构造器是私有的,不能被调用,则根据逻辑学,推导出枚举类不能有子类。
4.枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加。枚举类默认提供了一个values()方法,该方法可以很方便地遍历所有的枚举值。
枚举类的对象:SeasonEnum.SPRING,同时switch(object),还支持用对象最为判断条件,至少在枚举类这里是支持的!!!
所有的枚举类都继承了java.lang.Enum类,所以枚举类可以直接使用java.lang.Enum类中包含的方法:
- int compareTo(E o):该方法用于与指定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数,否则返回零。
2.String name():返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与此方法相比,大多数程序员应该优先考虑使用toString()方法,因为toString()方法返回更加用户友好的名称。String toString():返回枚举常量的名称,与name方法相似,但toString()方法更常用。
3.int ordinal():返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为零)。
4.public static<T extends Enum> T valueOf(Class enumType,String name):这是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。
正如前面看到的,当程序使用System.out.println(s)语句来打印枚举值时,实际上输出的是该枚举值的toString()方法,也就是输出该枚举值的名字。
枚举类也有成员变量,方法,构造器上面程序使用Gender枚举类时与使用一个普通类没有太大的差别,差别只是产生Gender对象的方式不同,用的是Enum.valueOf()方法,枚举类的实例只能是枚举值,而不是随意地通过new来创建枚举类对象。
面临问题:
首先Java应该把所有类设计成良好封装的类,所以不应该允许直接访问Gender类的name成员变量,而是应该通过方法来控制对name的访问。否则可能出现很混乱的情形,例如上面程序恰好 设置了g.name=“女”,要是采用g.name=“男”,那程序就会非常混乱了,可能出现FEMALE代表男的局面。
进一步可以将name设置为private的,然后加个set方法。
进一步优化,枚举类通常应该设计成不可变类,即它的成员变量值不应该允许改变,这样会更安全,且代码更加简洁。因此建议将枚举类的成员变量都使用private final修饰。
成员变量都使用了final修饰符来修饰===》必须在构造器里/定义成员变量/初始化块中时 为这些成员变量指定初始值,因此应该为枚举类显式定义带参数的构造器。
一旦为枚举类显式定义了带参数的构造器,列出枚举值时就必须对应地传入参数。
当为Gender枚举类创建了一个Gender(String name)构造器之后,在枚举类中列出枚举值时,实际上就是调用这个构造器创建枚举类对象,只是这里无须使用new关键字,也无须显式调用构造器。但是要传入参数了。如上所示。
学生提问:
枚举类不是用final修饰了吗?怎么还能派生子类呢?
答:
并不是所有的枚举类都使用了final修饰!非抽象的枚举类才默认使用final修饰。对于一个抽象的枚举类而言——只要它包含了抽 象方法,它就是抽象枚举类,系统会默认使用abstract修饰,而不是使用final修饰。
那么枚举类中可以定义抽象方法么?可以
假设有一个Operation枚举类,它的4个枚举值PLUS,MINUS,TIMES,DIVIDE分别代表加、减、乘、除4种运算,该枚举类需要定义一个eval()方法来完成计算。
Operation需要让PLUS、MINUS、TIMES、DIVIDE四个值对eval()方法各有不同的实现。此时可考虑为Operation枚举类定义一个eval()抽象方法,然后让4个枚举值分别为eval()提供不同的实现。
编译上面程序会生成5个class文件,其实Operation对应一个class文件,它的4个匿名内部子类分别各对应一个class文件。
枚举类里定义抽象方法时不能使用abstract关键字将枚举类定义成抽象类(因为系统自动会为它添加abstract关键字),但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。此时
PLUS、MINUS、TIMES、DIVIDE实际上是Operation枚举类的匿名子类的实例,而不是Operation类的实例。当调用MALE和FEMALE两个枚举值的方法时,就会看到两个枚举值的方法表现不同的行为方式。