5.9 枚举类
一、手动实现枚举类
在早期代码中,可能会直接使用简单的静态常量来表示枚举,例如:
1 public static final int SEASON_SPRING=1; 2 public static final int SEASON_SUMMER=2; 3 public static final int SEASON_FALL=3; 4 public static final int SEASON_WINTER=4;
存在问题:
1、类型不安全:因为上面每个季节都是一个整数,因此完全可以把一个季节当成一个整数使用,例如执行加法运算:SEASON_SPRING+SEASON_SUMMER
2、没有命名空间:当需要使用季节时,必须在SPRING前面使用SEASON_前缀,否则程序可能与其他类的静态常量混淆。
3、打印输出的意义不明确:当输出某个季节时,例如输出SEASON_SPRING,实际输出为1,这个1很难猜测它代表春天。
枚举有存在的意义,因此早期也可采用通过定义类的方式来实现,可采用以下设计方式:
1、通过private将构造器隐藏起来
2、把这个类的所有实例都使用public final static修饰符的类变量来保存。
3、如有必要,可提供一些静态方法,允许在其他程序根据特定参数来获取与之匹配的实例。
4、通过枚举类使程序更加健壮,避免创建对象的随意性。
二、枚举类入门
Java 5新增关键字enum,用于定义枚举。枚举是一种特殊的类,他一样可以拥有自己的成员变量、方法、可以实现一个或多个接口,也可以定义自己的构造器。
枚举类与普通类的区别:
(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枚举类:
1 public enum SeasonEnum 2 { 3 //在一行列出四个枚举类 4 SPRING,SUMMER,FALL,WINTER; 5 }
如果需要时使用该枚举类的某个实例,则可以使用EnumClass.variable的形式,如SeasonEnum,SPRING.
1 enum SeasonEnum 2 { 3 //在一行列举出4个枚举实例 4 SPRING,SUMMER,FALL,WINTER; 5 } 6 public class EnumTest 7 { 8 public void judge(SeasonEnum s) 9 { 10 // switch语句里的表达式可以是枚举值 11 switch (s) 12 { 13 case SPRING: 14 System.out.println("春暖花开,正好踏青"); 15 break; 16 case SUMMER: 17 System.out.println("夏日炎炎,适合游泳"); 18 break; 19 case FALL: 20 System.out.println("秋高气爽,进补及时"); 21 break; 22 case WINTER: 23 System.out.println("冬日雪飘,围炉赏雪"); 24 break; 25 } 26 } 27 public static void main(String[] args) 28 { 29 // 枚举类默认有一个values方法,返回该枚举类的所有实例 30 for (var s : SeasonEnum.values()) 31 { 32 System.out.println(s); 33 } 34 // 使用枚举实例时,可通过EnumClass.variable形式来访问 35 new EnumTest().judge(SeasonEnum.SPRING); 36 } 37 } 38 ---------- 运行Java捕获输出窗 ---------- 39 SPRING 40 SUMMER 41 FALL 42 WINTER 43 春暖花开,正好踏青 44 45 输出完成 (耗时 0 秒) - 正常终止
枚举类名.valueOf();这条语句返回枚举类的所有实例。
JDK 1.5对switch进行扩展:swith表达式可以是任何枚举类型,后面的case可以直接使用枚举值的名字,无需添加枚举类作为限定。
所有枚举类都继承java.lang.Enum类,因此所有枚举类都可以直接使用java.lang.Enum类的所有方法。java.lang.Enum类中提供的方法包括:
(1)``int compareTo(E o);``该方法返回枚举对象的比较顺序,同一个枚举实例只能与相同类型的枚举类进行比较。如果该枚举类位于指定枚举类之后,则会返回正数;如果枚举类位于指定枚举类之前,则会返回负数。
1 System.out.println((SeasonEnum.SUMMER).compareTo(SeasonEnum.SPRING));//输出1 2 System.out.println((SeasonEnum.SUMMER).compareTo(SeasonEnum.SUMMER));//输出0 3 System.out.println((SeasonEnum.SUMMER).compareTo(SeasonEnum.FALL));//输出-1 4 System.out.println((SeasonEnum.SUMMER).compareTo(SeasonEnum.WINTER));//输出-2
(2)``String name();``该方法返回此枚举实例的名称,这个名称就是定义枚举类时所列举出的所有枚举值之一。与此方法相比大多数程序员优先考虑toString()方法,因为toString()返回更加友好的名称。
1 System.out.println(SeasonEnum.SUMMER.name());//输出SUMMER 2 System.out.println(SeasonEnum.SUMMER.toString());//输出SUMMER
(3)''int ordinal();''返回枚举值在枚举类中的索引值(就是声明枚举值在枚举声明的位置,第一个枚举值的索引值为0)。
1 System.out.println(SeasonEnum.SUMMER.ordinal());//输出1
(4)''String toString();''返回枚举常量的名称,与name方法相似,但toString()方法更常用。
(5)``public static <T extends Enum<T>>T valueOf(Class<T>enumType,String name);``这是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与该枚举类声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。
System.out.println(Enum.valueOf(SeasonEnum.class,"SUMMER"));//输出SUMMER
三、枚举类的成员变量、方法、构造器
3.1 在枚举类中定义成员变量、方法
1 enum Gender 2 { 3 //列举出所有枚举类型 4 MALE,FEMALE; 5 6 //枚举类的实例变量,private修饰避免外部程序对实例变量进行修改, 7 //只能通过同一个类中的stter()方法进行修改 8 private String name; 9 //setter()方法,MALL只能设置为"男" 10 public void setName(String name) 11 { 12 switch(this) 13 { 14 case MALE: 15 if(name.equals("男"))//String类已经重写了equals()方法,只需要内容相同即可 16 this.name=name; 17 else 18 { 19 System.out.println("参数错误"); 20 return; 21 } 22 break; 23 case FEMALE: 24 if(name.equals("女"))//String类已经重写了equals()方法,只需要内容相同即可 25 this.name=name; 26 else 27 { 28 System.out.println("参数错误"); 29 return; 30 } 31 break; 32 } 33 } 34 public String getName() 35 { 36 return this.name; 37 } 38 } 39 public class GnenderTest 40 { 41 public static void main(String[] args) 42 { 43 Gender g=Enum.valueOf(Gender.class,"FEMALE");//FEMALE代表女 44 g.setName("女"); 45 System.out.println(g+"代表"+g.getName());//参数错误 46 g.setName("男"); 47 } 48 }
上面的做法不太好的一点是,所有成员枚举类应该设计成不可变类,因此建议它的成员变量不允许改变,这样会更安全,而且代码更加简洁。建议将枚举类的所有成员变量都使用private final修饰。
3.2 枚举类中定义构造器
如果将所有的成员变量都使用final修饰符来修饰,所以必须在构造器里为这些成员变量指定初始值(或在定义成员变量时指定默认值,或者在初始化块中指定初始值,但这两种情况并不常见),应该为枚举类型显示定义带参数的构造器。
一旦为枚举类型显示定义了带参数的构造器,列举出初始值时就必须传入参数
1 enum Gender1 2 { 3 //此处的枚举值必须调用对应的构造器来创建 4 MALE("男"),FEMALE("女"); 5 private final String name; 6 private Gender1(String name) 7 { 8 this.name=name; 9 } 10 public String getName() 11 { 12 return this.name; 13 } 14 } 15 public class Gender1Test 16 { 17 public static void main(String[] args) 18 { 19 Gender1 g=Enum.valueOf(Gender1.class,"MALE"); 20 System.out.println(g.getName());//男 21 } 22 }
当Gender1枚举类型创建了一个Gender1(String name)构造器之后,在枚举类列出枚举值时,实际上就是调用构造器创建枚举类对象,只是这里无需使用 new关键字,也无需显示调用构造器。
其实上面的列举出枚举类的所有枚举值等同于下面两行代码:
1 public static final Genger1 MALE=new Gender1("男"); 2 public static final Genger1 FEMALE=new Gender1("女");
四、实现接口中的枚举类
枚举类我们知道都是继承java.lang.Enum类,而普通类都是继承java.lang.Object类,因此枚举类不能继承普通类。但是枚举类可以实现一个或多个接口,枚举类实现一个或多个接口时,必须实现该接口所包含的所有抽象方法。
定义一个GenderDesc接口,该接口包含一个抽象方法。
1 public interface GenderDesc 2 { 3 void info(); 4 }
下面定义一个Gender2类实现GenderDesc接口中定义的info()方法:
1 enum Gender2 implements GenderDesc//枚举类默认会使用public static final修饰 2 { 3 //此处的枚举值必须调用对应的构造器来创建 4 MALE("男"),FEMALE("女"); 5 private final String name; 6 private Gender2(String name) 7 { 8 this.name=name; 9 } 10 public String getName() 11 { 12 return this.name; 13 } 14 public void info() 15 { 16 System.out.println("这是一个定义性别的类"); 17 } 18 } 19 public class Gender2Test 20 { 21 public static void main(String[] args) 22 { 23 Gender2 g=Enum.valueOf(Gender2.class,"MALE"); 24 System.out.println(g.getName());//男 25 g.info(); 26 27 Gender2 g1=Gender2.FEMALE; 28 System.out.println(g1.getName());//男 29 g1.info(); 30 } 31 } 32 男 33 这是一个定义性别的类 34 女 35 这是一个定义性别的类 36 请按任意键继续. . .
从上面代码可以看出,枚举类调用接口中的方法时都具有相同的行为方式。如果需要让每个枚举类调用该方法时具有不同的行为方式,则可以让每个枚举类分别实现该方法,每个枚举类提供不同的实现方法,从而让不同枚举类调用该方法时具有不同的行为方式:
1 enum GenderPlus implements GenderDesc//枚举类默认会使用public static final修饰 2 { 3 //此处的枚举值必须调用对应的构造器来创建 4 MALE("男") 5 //花括号部分实际上是一个类体部分 6 { 7 public void info() 8 { 9 System.out.println("这个枚举类代表男性"); 10 }}, 11 12 FEMALE("女"){ 13 public void info() 14 { 15 System.out.println("这个枚举类代表女性"); 16 }}; 17 18 private final String name; 19 private GenderPlus(String name) 20 { 21 this.name=name; 22 } 23 public String getName() 24 { 25 return this.name; 26 } 27 28 } 29 public class GenderPlusTest 30 { 31 public static void main(String[] args) 32 { 33 GenderPlus g=Enum.valueOf(GenderPlus.class,"MALE"); 34 System.out.println(g.getName());//男 35 g.info(); 36 37 GenderPlus g1=GenderPlus.FEMALE; 38 System.out.println(g1.getName());//女 39 g1.info(); 40 } 41 } 42 男 43 这个枚举类代表男性 44 女 45 这个枚举类代表女性 46 请按任意键继续. . .
上面程序中,在创建FEMALE和MALE两个枚举类时,后面又跟一个以堆花括号,这对花括号里就是info()方法定义。其实这就时匿名内部类的用法,花括好就是实现接口中的所有抽象方法的类体,在这种情况下,创建FEMALE、MALE实例时,并不是直接创建GenderPlus的实例,而是相当于创建GenderPlus的匿名子类的实例。
注意:枚举类使用final修饰,即不能派生子类,为什么上面使用匿名内部类生成了子类?
非抽象枚举类才使用final修饰,此时枚举类不能派生实例;对于一个抽象枚举类——只要它包含抽象方法,他就是抽象枚举类,系统默认使用abstarct修饰,从而失去了创建实例的功能,但获得了派生子类的功能。
编译上面的程序我们可以看出生成了GenderPlus.class、GenderPlus$1.class(匿名内部类1)、GenderPlus$2.class(匿名内部类2).这就证明了上面的结论:MALE,FEMALE实际上是GenderPlus的匿名子类的实例,而不是GenderPlus的实例。当调用MALE,FEMALE两个枚举类的info()方法时,将会出现不同的行为方式。
五、抽象枚举类
假设有一个Operation的枚举类,它的4个枚举值PLUS,MINUS,TIMES,DIVIDE分别代表加减乘除4种运算,该枚举类需要定义一个eval()方法完成计算。显然不同的枚举类eval(0方法具有不同的行为方式:
1 public enum Operation 2 { 3 Plus 4 { 5 public double eval(double x,double y) 6 { 7 return (x+y); 8 } 9 }, 10 MINUS 11 { 12 public double eval(double x,double y) 13 { 14 return (x-y); 15 } 16 }, 17 TIMES 18 { 19 public double eval(double x,double y) 20 { 21 return (x*y); 22 } 23 }, 24 DIVIDE 25 { 26 public double eval(double x,double y) 27 { 28 return (x/y); 29 } 30 }; 31 //为该枚举类定义一个抽象方法 32 public abstract double eval(double x,double y); 33 public static void main(String[] args) 34 { 35 System.out.println(Operation.Plus.eval(5,4)); 36 System.out.println(Operation.MINUS.eval(5,4)); 37 System.out.println(Operation.TIMES.eval(5,4)); 38 System.out.println(Operation.DIVIDE.eval(5,4)); 39 } 40 } 41 ---------- 运行Java捕获输出窗 ---------- 42 9.0 43 1.0 44 20.0 45 1.25 46 47 输出完成 (耗时 0 秒) - 正常终止
编译上面的程序会生成5个class文件,其实Operation抽象枚举类对应一个class文件,它的四个匿名内部类子类分别对应一个class文件。
枚举类里定义抽象方法时不能使用abstract关键字将枚举类型定义为抽象类(因为系统会自动添加abstract关键字),而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。