枚举类型是指由一组固定的常量组成的合法值的类型。例如一年中的季节,太阳系的行星或者一副牌的花色等,在还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具名的int常量,一个类型成员一个常量,如下所示:
1 public static final int APPLE_FUJI = 0; 2 public static final int APPLE_PIPPIN = 1; 3 4 public static final int ORANGE_NAVEL = 0; 5 public static final int ORANGE_TEMPLE = 1;
这样的方式叫做int枚举模式,这种模式有很多缺点:
⑴ 在类型的安全性和使用的方便性来说,对coder来说没有任何帮助;
⑵ 可以将不同代表性的值互相引用,并且编译器也不会出现警告(你可以将apple传到orange的方法中),这样即使程序不会出错,但对于代码的阅读者来说,很有可能会弄错;
⑶ 采用这种模式的程序十分脆弱,因为int枚举是编译时常量,被编译到使用它们的类中,如果与枚举常量关联的int发生了变化,那么这个类就必须重新编译,如果不重新编译,程序还是可以运行,但是运行的结果就不确定了(静态final常量在类编译的时候就确定了);
⑷ 遍历或者获得int枚举组的大小,没有很可靠的方法,调试或者打印出来的信息没有太大的用处,同时也很不方便;
枚举(enum)
可以替代这种模式,在避免了int枚举模式的缺点,同时提供了许多额外的好处。
从Java1.5开始,我们就可以使用枚举来代替上述的模式了,下面简单将上述例子修改为枚举类型:
public enum Apple{FUJI,PIPPIN} public enum Orange{NAVEL,TEMPLE}
Java枚举类型背后的基本想法非常简单:它们就是通过公有的静态final域为每个枚举常量导出实例的类。对Java来说,Java的枚举本质上是int值。
枚举具有很多特点:
⑴ 因为没有可以访问的构造器,枚举类型是真正的final。因为使用枚举的类既不能创建枚举的实例,也不能对其进行扩展,因此很有可能没有实例,而只有声明过的枚举常量。换句话说就是,枚举类型是实例受控的。它们是单例的泛型化,本质上是单元素的枚举;
⑵ 枚举提供了编译时的类型安全;如果声明一个参数的类型为Apple,就可以保证被传到该参数上的任何非null的对象引用一定属于两个有效的Apple值之一。试图传递类型错误的值时,会导致编译时错误,就像试图将某种枚举类型的表达式赋给另一种枚举常量的变量,或者试图利用==来比较不同枚举类型一样(避免了上述int枚举模式的第二种缺点)。
⑶ 包含同名常量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间(你可以在Apple里面定义一个WEIGHT,同时在Orange里面定义一个WEIGHT)。
⑷ enum的常量值并没有被编译到使用它们的类中,而是在int枚举模式中。这样在枚举类型和使用它们的类中提供了一个隔离层。你可以增加或者重新排列枚举类型的常量,无需重新编译使用它们的类的代码。而且,你还可以通过toString方法,将枚举类型转换成打印的字符串。
⑸ 枚举类型允许添加任意的方法和域,并且还可以实现任意的接口。提供了所有的Object方法的高级实现,实现了Comparable和Serializable接口,并且还对枚举类型的可任意改变性设计了序列化的方式。
⑹ 枚举与int常量相比,枚举有个小小的性能缺点,即在装载和初始化枚举时会有空间和时间的成本(除了资源受限的设备,例如手机和烤面包机外,在实际中不必太在意这个问题)。
前面4个特点比较简单,说说第五个特点,看个例子:
1 public enum Planet { 2 MERCURY(3.302e+23,2.439e6), 3 VENUS(4.869e+24,6.052e6), 4 EARTH(5.975e+24,6.378e6), 5 MARS(6.419e+23,3.393e6), 6 JUPITER(1.889e+27,7.149e7); 7 8 private final double mass; 9 private final double radius; 10 private final double surfaceGravity; 11 12 Planet(double mass,double radius){ 13 this.mass = mass; 14 this.radius = radius; 15 this.surfaceGravity = 10 * mass / (radius * radius); 16 } 17 18 public double mass(){ 19 return mass; 20 } 21 22 public double radius(){ 23 return radius; 24 } 25 26 public double surfaceGravity(){ 27 return surfaceGravity; 28 } 29 30 public double surfaceWeight(double mass){ 31 return mass * surfaceGravity; 32 } 33 }
编写一个像这样的Plant枚举类并不难。为了将数据与枚举常量联系起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,因此所有的域都应该是final的,这些域可以是公有的,但最好是私有的,并提供公有的访问方法。虽然这个枚举很简单,但是功能却很强大,如下所示:
1 public static void main(String[] args){ 2 double earthWeight = 175d; 3 double mass = earthWeight / Planet.EARTH.surfaceGravity(); 4 for (Planet p : Planet.values()){ 5 System.out.printf("Weight on %s is %f%n ",p,p.surfaceWeight(mass)); 6 } 7 }
输出结果:
1 Weight on MERCURY is 66.133672 2 Weight on VENUS is 158.383926 3 Weight on EARTH is 175.000000 4 Weight on MARS is 66.430699 5 Weight on JUPITER is 440.362707
可以看出通过一段简单的代码就可以实现很多的功能,并且对打印出来的信息一目了然。
EnumSet和EnumMap
enumSet和enumMap分别是枚举类型的set和map,看一下它们的用法:
1 public static void main(String[] args){ 2 EnumSet<Planet> es = EnumSet.allOf(Planet.class); 3 for (Planet ed : es) 4 System.out.println(ed.name() + ":" + ed.ordinal()); 5 6 System.out.println(" -----EnumSet和EnumMap之间的分隔线----- "); 7 8 EnumMap<Planet, String> em = new EnumMap<Planet, String>(Planet.class); 9 em.put(Planet.MERCURY, "水星"); 10 em.put(Planet.VENUS, "金星"); 11 em.put(Planet.EARTH, "地球"); 12 em.put(Planet.MARS, "火星"); 13 em.put(Planet.JUPITER, "木星"); 14 15 Iterator<Entry<Planet, String>> iterator = em.entrySet().iterator(); 16 while (iterator.hasNext()){ 17 Entry<Planet, String> entry = iterator.next(); 18 System.out.println(entry.getKey().name() + ":" + entry.getValue()); 19 } 20 }
输出结果:
MERCURY:0 VENUS:1 EARTH:2 MARS:3 JUPITER:4 -----EnumSet和EnumMap之间的分隔线----- MERCURY:水星 VENUS:金星 EARTH:地球 MARS:火星 JUPITER:木星
注意:
enumSet和enumMap都是非线程安全的。
总而言之,与int常量相比,枚举类型的优势是不言而喻的。枚举要以读得多,也更加安全,功能更加强大。每当需要一组固定常量的时候,就应该使用枚举(行星,一周的天数,棋子的数目等)。
参考:《Effective Java》中文版 第二版;