5.4 枚举的本质
5.4 枚举的本质
本节探讨Java中的枚举类型。枚举是一种特殊的数据,它的取值是有限的,是可以枚举出来的,比如一年有四季、一周有七天。虽然使用类也可以处理这种数据,但枚举类型更为简洁、安全和方便。下面介绍枚举的使用和实现原理。先介绍基础用法和原理,再介绍典型场景。
5.4.1 基础
定义和使用基本的枚举是比较简单的,我们来看个例子。为表示衣服的尺寸,我们定义一个枚举类型Size,包括三个尺寸:小、中、大,代码如下:
1 | public enum Size { |
枚举使用enum这个关键字来定义,Size包括三个值,分别表示小、中、大,值一般是大写的字母,多个值之间以逗号分隔。枚举类型可以定义为一个单独的文件,也可以定义在其他类内部。
可以这样使用Size:
1 | Size size = Size.MEDIUM |
Size size声明了一个变量size,它的类型是Size, size=Size.MEDIUM将枚举值MEDIUM赋值给size变量。枚举变量的toString方法返回其字面值,所有枚举类型也都有一个name()方法,返回值与toString()一样,例如:
1 | Size size = Size.SMALL; |
输出都是SMALL。枚举变量可以使用equals和==进行比较,结果是一样的,例如:
1 | Size size = Size.SMALL; |
上面代码的输出结果为三行,分别是true、true、false。枚举值是有顺序的,可以比较大小。枚举类型都有一个方法int ordinal(),表示枚举值在声明时的顺序,从0开始,例如,如下代码输出为1:
1 | Size size = Size.MEDIUM; |
另外,枚举类型都实现了Java API中的Comparable接口,都可以通过方法compareTo与其他枚举值进行比较。比较其实就是比较ordinal的大小,例如,如下代码输出为-1,表示SMALL小于MEDIUM:
1 | Size size = Size.SMALL; |
枚举变量可以用于和其他类型变量一样的地方,如方法参数、类变量、实例变量等。枚举还可以用于switch语句,代码如下所示:
1 | static void onChosen(Size size){ |
在switch语句内部,枚举值不能带枚举类型前缀,例如,直接使用SMALL,不能使用Size.SMALL。枚举类型都有一个静态的valueOf(String)方法,可以返回字符串对应的枚举值,例如,以下代码输出为true:
1 | System.out.println(Size.SMALL==Size.valueOf("SMALL")); |
枚举类型也都有一个静态的values方法,返回一个包括所有枚举值的数组,顺序与声明时的顺序一致,例如:
1 | for(Size size : Size.values()){ |
屏幕输出为三行,分别是SMALL、MEDIUM、LARGE。
Java是从Java 5才开始支持枚举的,在此之前,一般是在类中定义静态整型变量来实现类似功能,代码如下所示:
1 | class Size { |
枚举的好处体现在以下几方面。
- 定义枚举的语法更为简洁。
- 枚举更为安全。一个枚举类型的变量,它的值要么为null,要么为枚举值之一,不可能为其他值,但使用整型变量,它的值就没有办法强制,值可能就是无效的。
- 枚举类型自带很多便利方法(如values、valueOf、toString等),易于使用。
枚举是怎么实现的呢?枚举类型实际上会被Java编译器转换为一个对应的类,这个类继承了Java API中的java.lang.Enum类。Enum类有name和ordinal两个实例变量,在构造方法中需要传递,name()、toString()、ordinal()、compareTo()、equals()方法都是由Enum类根据其实例变量name和ordinal实现的。values和valueOf方法是编译器给每个枚举类型自动添加的,上面的枚举类型Size转换成的普通类的代码大概如代码清单5-12所示。需要说明的是,这只是示意代码,不能直接运行。
1 | public final class Size extends Enum<Size> { |
解释几点:
1)Size是final的,不能被继承,Enum<Size>
表示父类,<Size>
是泛型写法;
2)Size有一个私有的构造方法,接受name和ordinal,传递给父类,私有表示不能在外部创建新的实例;
3)三个枚举值实际上是三个静态变量,也是final的,不能被修改;
4)values方法是编译器添加的,内部有一个values数组保持所有枚举值;
5)valueOf方法调用的是父类的方法,额外传递了参数Size.class,表示类的类型信息,关于类型信息的详细介绍在第21章,父类实际上是回过头来调用values方法,根据name对比得到对应的枚举值的。
一般枚举变量会被转换为对应的类变量,在switch语句中,枚举值会被转换为其对应的ordinal值。可以看出,枚举类型本质上也是类,但由于编译器自动做了很多事情,因此它的使用更为简洁、安全和方便。
5.4.2 典型场景
以上枚举用法是最简单的,实际中枚举经常会有关联的实例变量和方法。比如,上面的Size例子,每个枚举值可能有关联的缩写和中文名称,可能需要静态方法根据缩写返回对应的枚举值,修改后的Size代码如代码清单5-13所示。
1 | public enum Size { |
上述代码定义了两个实例变量abbr和title,以及对应的get方法,分别表示缩写和中文名称;定义了一个私有构造方法,接受缩写和中文名称,每个枚举值在定义的时候都传递了对应的值;同时定义了一个静态方法fromAbbr,根据缩写返回对应的枚举值。需要说明的是,枚举值的定义需要放在最上面,枚举值写完之后,要以分号(; )结尾,然后才能写其他代码。
这个枚举定义的使用与其他类类似,比如:
1 | Size s = Size.MEDIUM; |
加了实例变量和方法后,枚举转换后的类与代码清单5-12类似,只是增加了对应的变量和方法,修改了构造方法,代码不同之处大概如代码清单5-14所示。
1 | public final class Size extends Enum<Size> { |
每个枚举值经常有一个关联的标识符(id),通常用int整数表示,使用整数可以节约存储空间,减少网络传输。一个自然的想法是使用枚举中自带的ordinal值,但ordinal值并不是一个好的选择。为什么呢?因为ordinal值会随着枚举值在定义中的位置变化而变化,但一般来说,我们希望id值和枚举值的关系保持不变,尤其是表示枚举值的id已经保存在了很多地方的时候。比如,上面的Size例子,Size.SMALL的ordinal值为0,我们希望0表示的就是Size.SMALL,但如果增加一个表示超小的值XSMALL:
1 | public enum Size { |
这时,0就表示XSMALL了。所以,一般是增加一个实例变量表示id。使用实例变量的另一个好处是,id可以自己定义。比如,Size例子可以写为:
1 | public enum Size { |
枚举还有一些高级用法,比如,每个枚举值可以有关联的类定义体,枚举类型可以声明抽象方法,每个枚举值中可以实现该方法,也可以重写枚举类型的其他方法。此外,枚举可以实现接口,也可以在接口中定义枚举,其使用相对较少,我们就不介绍了。
至此,关于枚举,我们就介绍完了,对于枚举类型的数据,虽然直接使用类也可以处理,但枚举类型更为简洁、安全和方便。
本章介绍了类的一些扩展概念,包括接口、抽象类、内部类和枚举。我们之前提到过异常,但并未深入讨论,让我们下一章来探讨。