一、枚举简介
某些情况下,类的对象有限且固定,比如季节,有春夏秋冬四个对象,行星类目前只有8个对象,因此,像这些实例固定且有限的类称为枚举类。
在
JDK 1.5
之前没有枚举类型,那时候一般用接口常量来替代,如:
public static final int SEASON_SPRING=1;
public static final int SEASON_SUMMER=2;
public static final int SEASON_FALL=3;
public static final int SEASON_WINTER=4;
这种方式虽然定义简单,但是类型不安全,季节实际上是一个int的整数,但是int整数之间可以加减,那么进行
SEASON_SPRING+SEASON_SUMMER
,这样的代码完全正常,但是不合常理,还有打印输出不明确,当打印季节
SEASON_SPRING
时,实际上打印了数字1。而使用
Java
枚举的出现可以更恰当地表示该常量。
使用
enum
关键词定义,与
class
、
interface
地位是一样的,枚举类也是一种特殊的类,它一样可以有自己的成员变量、方法,可以实现一个或者多个接口,也可以定义自己的构造器。一个
Java
源文件最多只能定义一个
public
访问权限的枚举类,且该
Java
源文件也必须和该枚举类的类名相同。
但是枚举终究不是普通的类,与普通的类有着如下的简单区别:
- 枚举类可以实现一个或者多个接口,使用
定义的枚举类默认继承了enum
类,而不是默认继承了java.lang.Enum
类,因此枚举类不能显式继承其他父类,其中Object
类实现了java.lang.Enum
和java.lang.Serializable
两个接口;java.lang.Comparable
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
}
既然枚举类都继承了
java.lang.Enum
类,所以枚举类可以直接使用
java.lang.Enum
类中包含的方法,
java.lang.Enum
类有如下几种方法 :
int compare(E o)
:该方法用于与指定对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数;否则返回0;
String name()
:返回此枚举实例的名称,这个名称就是定义枚举类时列出所有的枚举值之一。与此方法相比,大多数程序员优先考虑
toString()
方法,因为
toString()
方法返回更加用户友好的名称
int ordinal()
:返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值得索引值为0);
String toString()
:返回枚举常量的名称,与name方法相似,但是
toString()
方法更加常用;
public static<T extends Enum<T>>T valueOf(Class<T>enumType,String name)
:这是一个静态方法,用于返回指定枚举类中指定名称的枚举值,名称必须与该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。
当程序员使用
System.out.println(s)
语句来打印枚举值时,实际上输出的是该枚举值的
toString()
方法,也就是输出该枚举值的名字。
- 使用
定义、非抽象的枚举类默认会使用final修饰,因此枚举类不能派生子类(对于抽象的枚举类而言,系统会默认使用enum
修饰,而不是abstract
修饰);final
- 枚举类的构造器只能用
访问控制符,如果省略了构造器的访问控制符,默认为private
;如果强制指定访问控制符,则只能指定private
修饰符;private
- 枚举类的所有实例必须在枚举类第一行显式列出,否则这个枚举类不能产生实例,列出这些实例时,系统自动加上
public static final
修饰,无需程序员显式添加;
枚举类提供了一个
方法,该方法可以很方便地遍历所有的枚举值:value()
public enum Season {
//列出枚举的4个实例
SPRING,SUMMER,FALL,WINTER;
}
public class TestEnum {
public void judge(Season s){
switch (s){
case SPRING:
System.out.println("春暖花开,正好踏春!");
break;
case SUMMER:
System.out.println("夏日炎炎,适合游泳!");
break;
case FALL:
System.out.println("秋高气爽,进补及时!");
break;
case WINTER:
System.out.println("冬日雪飘,围炉赏雪!");
break;
}
}
public static void main(String[]args){
//枚举类默认有一个values()方法,返回该枚举类的所有实例
for(Season s:Season.values()){
System.out.println(s);
}
//使用枚举实例时,可以通过EnumClass.variable形式来访问
new TestEnum().judge(Season.SPRING);
}
}
运行结果:
SPRING
SUMMER
FALL
WINTER
春暖花开,正好踏春!
上述程序测试了
Season
枚举类的用法,该类通过了
values()
方法返回了
Season
枚举类的所有实例,并通过循环迭代输出了枚举类的所有的实例
不仅如此,上面程序的switch表达式中还使用了
Season
对象作为表达式,这是
JDK1.5
增加枚举后对于
switch
的扩展:
switch
的控制表达式可以是任何枚举类型,当
switch
控制表达式使用枚举类型时,后面的
case
表达式中的值直接使用枚举值的名字,无需添加枚举类作为限定。
二、枚举的成员变量、方法和构造器
枚举类也是一种类,知识它是比较特殊的类,因此它一样可以定义成员变量、方法和构造器。下面以定义一个
People
枚举类,该枚举类里包含了一个
name
实例变量。
public enum People {
MALE,FEMALE;
//定义一个String类型的成员变量
public String name;
}
public class TestPeople {
public static void main(String[]args){
People p=People.valueOf(People.class,"MALE");
//直接给枚举的成员变量赋值
p.name="女";
//访问枚举的name实例变量
System.out.println(p+"表示:"+p.name);
}
}
运行结果:
MALE表示:女
并不能随意通过
new
来创建枚举类的对象,
Java
应该把所有类设计成良好封装的类,所以不应该直接访问
People
类的
name
成员变量,而是应该通过方法来控制对
name
的访问,否则可能很混乱的情形,比如将
p.name="男"
,就会表示
FEMALE
代表男的局面,因此可以用改进的
People
类设计
public enum People {
MALE,FEMALE;
//定义一个String类型的成员变量
private String name;
public void setName(String name){
switch(this){
case MALE:
if(name.equals("男")){
this.name=name;
}else{
System.out.println("参数错误!");
return;
}
break;
case FEMALE:
if(name.equals("女")){
this.name=name;
}else{
System.out.println("参数错误!");
return;
}
break;
}
}
public String getName(){
return this.name;
}
}
public class TestPeople {
public static void main(String[]args){
People p=People.valueOf(People.class,"MALE");
p.setName("女");
System.out.println(p+"表示:"+p.getName());
p.setName("男");
System.out.println(p+"表示:"+p.getName());
}
}
运行结果:
参数错误!
MALE表示:null
MALE表示:男
上述代码通过
get
和
set
方法将
FEMALE
枚举值得
name
变量设置为“男”,系统设置会提示错误信息,实际上这种做法还是不够好,枚举类通常应该设计成不可变的类,也就是说成员变量值不应该允许改变,这样会更加安全,代码也会更加简洁,因此建议将枚举类的成员变量都使用
private final
修饰。
如果将所有的成员变量都使用了
final
修饰符来修饰,所有必须在构造器里为这些成员变量指定初始值(或者在定义成员变量时指定默认值,或者在初始化块中指定初始值,但是这两种情况并不常见),因此应该为枚举类显式定义带参数的构造器,一旦为枚举类显式定义了带有参数的构造器,列出枚举值得时候就必须对应地传入参数。
public enum People {
//此处必须调用对应的枚举构造器来创建
MALE("男"),FEMALE("女");
private final String name;
//枚举类的构造器必须用private修饰
private People(String name){
this.name=name;
}
public String getName(){
return this.name;
}
}
从上面的程序中可以看出,当为
People
枚举类创建一个
People(String name)
构造器之后,列出枚举值就应该采用
MALE("男"),FEMALE("女");
来完成,也就是说,在枚举类汇总列出枚举值时,实际上就是调用了构造器创建枚举类的对象,只是这里无须使用
new
关键词,也无须显式调用构造器,前面列出枚举值时无须传入参数,甚至无须使用括号,仅仅是因为前面的枚举类包含无参数的构造器
上面一行代码等同于:
public static final People MALE=new People("男");
public static final People FEMALE=new People("女");
三、实现接口的枚举类
枚举类也可以实现一个或者多个接口,与普通类实现一个或多个接口完全一样,枚举类实现一个或者多个接口时,也可以实现该接口所包含的方法,
interface GenderDesc{
void info();
}
public enum Gender implements GenderDesc {
MALE("男"),FEMALE("女");
//实现接口方法
public void info(){
System.out.println("这是一个用于定义性别的枚举类");
}
private final String name;
private Gender(String name){
this.name = name;
}
public String getName(){
return this.name;
}
}
如果由枚举类来实现接口的方法,则每个枚举值在调用该方法时都会有同样的行为方式(因为方法体完全一样),如果需要每个枚举值在调用该方法时呈现不同的行为方式,则可以让每个枚举值分别来实现该方法,每个枚举值提供不同的实现方式,从而让不同的枚举值调用该方法时具有不同的行为方式,在下面的
Genden
枚举类中,不同的枚举值对于
info()
方法的实现各不相同
interface GenderDesc{
void info();
}
public enum Gender implements GenderDesc {
MALE("男"){
@Override
public void info() {
System.out.println("male info");
}
},
FEMALE("女"){
@Override
public void info() {
System.out.println("female info");
}
};
private final String name;
private Gender(String name){
this.name = name;
}
public String getName(){
return this.name;
}
}
上面这种方式,
{}
相当于创建
Gender
的匿名子类的实例,
MALE
和
FEMALE
实际上是
Gender
匿名子类的实例,而不是
Gender
类的实例,当调用了
MALE
和
FEMALE
两个枚举值的方法时,就会看到两个枚举值的方法表现不同的行为方式。
四、包含抽象方法的枚举类
假设有一个
Operation
枚举类,它的4个枚举值
PLUS,MINUS,TIMES,DIVIDE
分别代表加减乘除4种运算,该枚举类需要定义一个
eval()
方法来实运算。
综上所述,可以考虑为
Operation
枚举类定义一个
eval()
抽象方法,然后让4个枚举值分别为
eval()
提供不同的实现:
public enum Operation {
PLUS{
@Override
public int eval(int a, int b) {
return a + b;
}
},
MINUS{
@Override
public int eval(int a, int b) {
return a - b;
}
},
TIMES{
@Override
public int eval(int a,int b){
return a*b;
}
},
DIVIDE{
@Override
public int eval(int a,int b){
return a/b;
}
};
//定义一个抽象方法,
//每个枚举值都提供不同的实现
public abstract int eval(int a, int b);
public static void main(String[] args){
System.out.println(Operation.PLUS.eval(10, 2));
System.out.println(Operation.MINUS.eval(10, 2));
System.out.println(Operation.TIMES.eval(10, 2));
System.out.println(Operation.DIVIDE.eval(10, 2));
}
}
运行结果:
12
8
20
5
它的4个匿名内部子类分别对应一个
class
文件,枚举类里定义抽象方法时不能使用
abstract
关键词将枚举定义抽象类(因为系统自动会为它添加
abstract
关键词),但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。
枚举集合
java.util.EnumSet
和
java.util.EnumMap
是两个枚举集合。
EnumSet
保证集合中的元素不重复;
EnumMap
中的
key
是
enum
类型,而
value
则可以是任意类型。
//定义数据库类型枚举
public enum DataBaseType
{
MYSQUORACLE,DB2,SQLSERVER
}
//某类中定义的获取数据库URL的方法以及EnumMap的声明
private EnumMap<DataBaseType,String>urls=new EnumMap<DataBaseType,String>(DataBaseType.class);
public DataBaseInfo()
{
urls.put(DataBaseType.DB2,"jdbc:db2://localhost:5000/sample");
urls.put(DataBaseType.MYSQL,"jdbc:mysql://localhost/mydb");
urls.put(DataBaseType.ORACLE,"jdbc:oracle:thin:@localhost:1521:sample");
urls.put(DataBaseType.SQLSERVER,"jdbc:microsoft:sqlserver://sql:1433;Database=mydb");
}
//根据不同的数据库类型,返回对应的URL
//@param type DataBaseType 枚举类新实例
//@return
public String getURL(DataBaseType type)
{
return this.urls.get(type);
}
在实际使用中,
EnumMap
对象
urls
往往是由外部负责整个应用初始化的代码来填充的。这里为了演示方便,类自己做了内容填充。从本例中可以看出,使用
EnumMap
可以很方便地为枚举类型在不同的环境中绑定到不同的值上。本例子中
getURL
绑定到
URL
上,在其他的代码中可能又被绑定到数据库驱动上去。
EnumSet
是枚举类型的高性能
Set
实现,它要求放入它的枚举常量必须属于同一枚举类型。
EnumSet
提供了许多工厂方法以便于初始化,如图所示:
EnumSet
作为
Set
接口实现,它支持对包含的枚举常量的遍历:
for(Operation op:EnumSet.range(Operation.PLUS,Operation.MULTIPLY)){
doSomeThing(op);
}