5.9 枚举类

在某些情况下,一个类的对象是有限而且固定的,比如季节类只有四个对象。这种实例有限而且固定的类,在Java里被称为枚举类。

一、手动实现枚举类

在早期代码中,可能会直接使用简单的静态常量来表示枚举,例如:

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 秒) - 正常终止
View Code

 枚举类名.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
View Code

(2)``String name();``该方法返回此枚举实例的名称,这个名称就是定义枚举类时所列举出的所有枚举值之一。与此方法相比大多数程序员优先考虑toString()方法,因为toString()返回更加友好的名称。

1 System.out.println(SeasonEnum.SUMMER.name());//输出SUMMER
2 System.out.println(SeasonEnum.SUMMER.toString());//输出SUMMER
View Code

(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 }
View Code

 上面的做法不太好的一点是,所有成员枚举类应该设计成不可变类,因此建议它的成员变量不允许改变,这样会更安全,而且代码更加简洁。建议将枚举类的所有成员变量都使用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 }
View Code

当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 请按任意键继续. . .
View Code

从上面代码可以看出,枚举类调用接口中的方法时都具有相同的行为方式。如果需要让每个枚举类调用该方法时具有不同的行为方式,则可以让每个枚举类分别实现该方法,每个枚举类提供不同的实现方法,从而让不同枚举类调用该方法时具有不同的行为方式:

 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 请按任意键继续. . .
View Code

上面程序中,在创建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 秒) - 正常终止
View Code

 编译上面的程序会生成5个class文件,其实Operation抽象枚举类对应一个class文件,它的四个匿名内部类子类分别对应一个class文件。

枚举类里定义抽象方法时不能使用abstract关键字将枚举类型定义为抽象类(因为系统会自动添加abstract关键字),而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。

posted @ 2020-03-09 13:17  小新和风间  阅读(164)  评论(0编辑  收藏  举报