java基础—枚举类

枚举类

  在某些情况下, 一个类的对象是有限而且固定的, 比如季节类, 它只有4个对象;再比如行星类,目前只有8个对象。这种实例有限而且固定的类,在Java里被称为枚举类。

1.1、手动实现枚举类

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

public static final int SEASON_SPRING = 1;
public static final int SEASON_SUMMER = 1;
public static final int SEASON_FALL = 1;
public static final int SEASON_WINTER = 1;

  这种定义方法简单明了,但存在如下几个问题。
  ➢ 类型不安全:因为上面的每个季节实际上是一个int整数, 因此完全可以把一个季节当成一个int整数使用,例如进行加法运 算SEASON_SPRING+SEASON_SUMMER,这样的代码完全正常。
  ➢ 没有命名空间:当需要使用季节时, 必须在SPRING前使用 SEASON_前缀,否则程序可能与其他类中的静态常量混淆。
  ➢ 打印输出的意义不明确:当输出某个季节时, 例如输出 SEASON_SPRING,实际上输出的是1,这个1很难猜测到它代表了 春天。 但枚举又确实有存在的意义, 因此早期也可采用通过定义类的方式来实现,可以采用如下设计方式。
    ➢ 通过private将构造器隐藏起来。
    ➢ 把这个类的所有可能实例都使用public static final修饰的类变量来保存。
    ➢ 如果有必要,可以提供一些静态方法,允许其他程序根据特定参数来获取与之匹配的实例。
    ➢ 使用枚举类可以使程序更加健壮,避免创建对象的随意性。
  但通过定义类来实现枚举的代码量比较大, 实现起来也比较麻 烦,Java从JDK 1.5后就增加了对枚举类的支持。

1.2、枚举类入门

  Java 5新增了一个enum关键字(它与class、interface关键字的 地位相同),用以定义枚举类。正如前面看到的,枚举类是一种特殊的类,它一样可以有自己的成员变量、方法,可以实现一个或者多个 接口, 也可以定义自己的构造器。 一个Java源文件中最多只能定义一 个public访问权限的枚举类, 且该Java源文件也必须和该枚举类的类名相同。
  但枚举类终究不是普通类,它与普通类有如下简单区别。
  ➢ 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类, 而不是默认继承Object类, 因此枚举类不能显式继承其他父类。 其中java.lang.Enum类实现了 java.lang.Serializable和java.lang.Comparable两个接口。
  ➢ 使用enum定义、非抽象的枚举类默认会使用final修饰。
  ➢ 枚举类的构造器只能使用private访问控制符, 如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。由于枚举类的所有构造 器都是private的,而子类构造器总要调用父类构造器一次,因此枚举类不能派生子类。
  ➢ 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个 枚举类永远都不能产生实例。 列出这些实例时, 系统会自动添 加public static final修饰,无须程序员显式添加。
  枚举类默认提供了一个values()方法, 该方法可以很方便地遍历所有的枚举值。
  下面程序定义了一个SeasonEnum枚举类。

public enum SeasonEnum {
    // 在第一行列出4个枚举实例
    SPRING, SUMMER, FALL, WINTER;
}

  编译上面Java程序, 将生成一个SeasonEnum.class文件, 这表明 枚举类是一个特殊的Java类。 由此可见, enum关键字和class、 interface关键字的作用大致相似。
  定义枚举类时, 需要显式列出所有的枚举值, 如上面的SPRING, SUMMER,FALL,WINTER;所示,所有的枚举值之间以英文逗号(,)隔 开,枚举值列举结束后以英文分号作为结束。这些枚举值代表了该枚举类的所有可能的实例。
  如 果 需 要 使 用 该 枚 举 类 的 某 个 实 例 , 则 可 使 用 EnumClass.variable的形式,如SeasonEnum.SPRING。

public class EnumTest {
    public void judge(SeasonEnum s) {
        // switch语句里的表达式可以是枚举值
        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 (SeasonEnum s : SeasonEnum.values()) {
            System.out.println(s);
        }
        // 使用枚举实例时,可通过EnumClass.variable形式来访问
        new EnumTest().judge(SeasonEnum.SPRING);
    }
}

  上面程序测试了SeasonEnum枚举类的用法, 该类通过values()方 法返回了SeasonEnum枚举类的所有实例, 并通过循环迭代输出了 SeasonEnum枚举类的所有实例。
  不仅如此,上面程序的switich表达式中还使用了SeasonEnum对象 作为表达式,这是JDK 1.5增加枚举后对switch的扩展:switch的控制 表达式可以是任何枚举类型。 不仅如此, 当switch控制表达式使用枚 举类型时, 后面case表达式中的值直接使用枚举值的名字, 无须添加 枚举类作为限定。

  前面已经介绍过, 所有的枚举类都继承了java.lang.Enum类, 所 以 枚 举 类 可 以 直 接 使 用 java.lang.Enum 类 中 所 包 含 的 方 法 。 java.lang.Enum类中提供了如下几个方法。
  ➢ int compareTo(E o):该方法用于与指定枚举对象比较顺序, 同一个枚举实例只能与相同类型的枚举实例进行比较。 如果该 枚举对象位于指定枚举对象之后, 则返回正整数;如果该枚举 对象位于指定枚举对象之前,则返回负整数,否则返回零。
  ➢ String name():返回此枚举实例的名称, 这个名称就是定义 枚举类时列出的所有枚举值之一。 与此方法相比, 大多数程序 员应该优先考虑使用toString()方法, 因为toString()方法返 回更加用户友好的名称。
  ➢ int ordinal():返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为零)。
  ➢ String toString():返回枚举常量的名称, 与name方法相 似,但toString()方法更常用。
  ➢ public static<T extends Enum> T valueOf(Class enumType, String name):这是一个静态方法, 用于返回指定 枚举类中指定名称的枚举值。 名称必须与在该枚举类中声明枚 举值时所用的标识符完全匹配,不允许使用额外的空白字符。
  正如上面查询循环输出SeasonEnum的枚举值,当程序使System.out.println(s)语句来打印枚举值时, 实际上输出的是该枚举值的toString()方法, 也就是输出该枚举值的名字。

1.3、枚举类的成员变量、方法和构造器

  枚举类也是一种类, 只是它是一种比较特殊的类, 因此它一样可 以定义成员变量、方法和构造器。 下面程序将定义一个Gender枚举 类,该枚举类里包含了一个name实例变量。

public enum Gender {
    MALE, FEMALE;
    // 定义一个public修饰的实例变量
    public String name;
}

  上面的Gender枚举类里定义了一个名为name的实例变量, 并且将 它定义成一个public访问权限的。 下面通过如下程序来使用该枚举类。

public class GenderTest {
    public static void main(String[] args) {
        // 通过Enum的valueOf()方法来获取指定枚举类的枚举值
        Gender g = Enum.valueOf(Gender.class, "FEMALE");
        // 直接为枚举值的name实例变量赋值
        g.name = "女";
        // 直接访问枚举值的name实例变量
        System.out.println(g + "代表:" + g.name);
    }
}

  上面程序使用Gender枚举类时与使用一个普通类没有太大的差 别, 差别只是产生Gender对象的方式不同, 枚举类的实例只能是枚举 值,而不是随意地通过new来创建枚举类对象。
  正如前面提到的, Java应该把所有类设计成良好封装的类, 所以 不应该允许直接访问Gender类的name成员变量, 而是应该通过方法来 控制对name的访问。 否则可能出现很混乱的情形, 例如上面程序恰好 设置了g.name=" 女" , 要是采用g.name=" 男" , 那程序就会非常混乱 了, 可能出现FEMALE代表男的局面。 可以按如下代码来改进Gender类 的设计。

public enum Gender {
    MALE, FEMALE;
    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;
    }
}

  上面程序把name设置成private,从而避免其他程序直接访问该 name成员变量, 必须通过setName()方法来修改Gender实例的name变 量,而setName()方法就可以保证不会产生混乱。上面程序中粗体字部 分保证FEMALE枚举值的name变量只能设置为" 女" , 而MALE枚举值的 name变量则只能设置为"男"。测试如下程序。

public class GenderTest {
    public static void main(String[] args) {
        Gender g = Gender.valueOf("FEMALE");
        g.setName("女");
        System.out.println(g + "代表:" + g.getName());
        // 此时设置name值时将会提示参数错误。
        g.setName("男");
        System.out.println(g + "代表:" + g.getName());
    }
}

  实际上这种做法依然不够好, 枚举类通常应该设计成不可变类, 也就是说,它的成员变量值不应该允许改变,这样会更安全,而且代 码更加简洁。 因此建议将枚举类的成员变量都使用private final修饰。
  如果将所有的成员变量都使用了final修饰符来修饰,所以必须在 构造器里为这些成员变量指定初始值(或者在定义成员变量时指定默 认值,或者在初始化块中指定初始值,但这两种情况并不常见),因此应该为枚举类显式定义带参数的构造器。
  一旦为枚举类显式定义了带参数的构造器, 列出枚举值时就必须对应地传入参数。示例如下:

public enum Gender {
    // 1——此处的枚举值必须调用对应构造器来创建
    MALE("男"), FEMALE("女");
    private final String name;

    // 枚举类的构造器只能使用private修饰
    private Gender(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

  从 上 面 程 序 中 可 以 看 出 , 当 为 Gender 枚 举 类 创 建 了 一 个 Gender(String name)构造器之后,列出枚举值就应该采用粗体字代码 来完成。也就是说,在枚举类中列出枚举值时,实际上就是调用构造 器创建枚举类对象,只是这里无须使用new关键字,也无须显式调用构 造器。前面列出枚举值时无须传入参数,甚至无须使用括号,仅仅是 因为前面的枚举类包含无参数的构造器。
不难看出,上面程序标号为"1"的代码实际上等同于如下两行代码:

public static final Gender MALE = new Gender("男");
public static final Gender FEMLE = new Gender("女");

1.4、实现接口的枚举类

  枚举类也可以实现一个或多个接口。 与普通类实现一个或多个接 口完全一样,枚举类实现一个或多个接口时,也需要实现该接口所包 含的方法。下面程序定义了一个GenderDesc接口。

public interface GenderDesc {
    void info();
}

  在上面GenderDesc接口中定义了一个info()方法, 下面的Gender 枚举类实现了该接口, 并实现了该接口里包含的info()方法。 下面是 Gender枚举类的代码。

public enum Gender implements GenderDesc {
    MALE, FEMALE;
    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;
        }
    }
     
	// 增加下面的info()方法,实现GenderDesc接口必须实现的方法
	public void info(){
		System.out.println("这是一个用于用于定义性别的枚举类");
	}
}

  读者可能会发现, 枚举类实现接口不过如此, 与普通类实现接口 完全一样:使用implements实现接口, 并实现接口里包含的抽象方 法。
  如果由枚举类来实现接口里的方法, 则每个枚举值在调用该方法 时都有相同的行为方式(因为方法体完全一样)。如果需要每个枚举 值在调用该方法时呈现出不同的行为方式,则可以让每个枚举值分别 来实现该方法,每个枚举值提供不同的实现方式,从而让不同的枚举 值调用该方法时具有不同的行为方式。 在下面的Gender枚举类中, 不同的枚举值对info()方法的实现各不相同。

public enum Gender implements GenderDesc {
    //	此处的枚举值必须调用对应构造器来创建
    //	MALE("男"), FEMALE("女");
    // 此处的枚举值必须调用对应构造器来创建
    MALE("男")
            // 花括号部分实际上是一个类体部分
            {
                public void info() {
                    System.out.println("这个枚举值代表男性");
                }
            },
    FEMALE("女") {
        public void info() {
            System.out.println("这个枚举值代表女性");
        }
    };
    
    private final String name;

    // 枚举类的构造器只能使用private修饰
    private Gender(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
    
	// 增加下面的info()方法,实现GenderDesc接口必须实现的方法
	public void info(){
		System.out.println("这是一个用于用于定义性别的枚举类");
	}
}

  当创建MALE和FEMALE两 个枚举值时, 后面又紧跟了一对花括号, 这对花括号里包含了一个 info()方法定义。 如果读者还记得匿名内部类语法的话, 则可能对这 样的语法有点印象了,花括号部分实际上就是一个类体部分,在这种情况下, 当创建MALE、FEMALE枚举值时, 并不是直接创建Gender枚举类的实例, 而是相当于创建Gender的匿名子类的实例。
疑问:枚举类不是用final修饰了吗?怎么还能派生子类呢?
  并不是所有的枚举类都使用了final修饰!非抽象的枚举类才默 认使用final修饰。对于一个抽象的枚举类而言——只要它包含了抽 象方法,它就是抽象枚举类,系统会默认使用abstract修饰,而不是使用final修饰。
  编 译 上 面 的 程 序 , 可 以 看 到 生 成 了 Gender.class 、 Gender$1.class和Gender$2.class三个文件,这样的三个class文件正 好证明了上面的结论:MALE和FEMALE实际上是Gender匿名子类的实 例, 而不是Gender类的实例。 当调用MALE和FEMALE两个枚举值的方法 时,就会看到两个枚举值的方法表现不同的行为方式。

1.5、包含抽象方法的枚举类

  假设有一个Operation枚举类, 它的4个枚举值PLUS, MINUS, TIMES, DIVIDE分别代表加、减、乘、除4种运算, 该枚举类需要定义 一个eval()方法来完成计算。
  从上面描述可以看出, Operation需要让PLUS、MINUS、TIMES、 DIVIDE四个值对eval()方法各有不同的实现。此时可考虑为Operation 枚举类定义一个eval()抽象方法,然后让4个枚举值分别为eval()提供 不同的实现。例如如下代码。

public enum Operation {
    PLUS {
        public double eval(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        public double eval(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        public double eval(double x, double y) {
            return x * y;
        }
    },
    DIVIDE {
        public double eval(double x, double y) {
            return x / y;
        }
    };

    // 为枚举类定义一个抽象方法
    // 1——这个抽象方法由不同的枚举值提供不同的实现
    public abstract double eval(double x, double y);

    public static void main(String[] args) {
        System.out.println(Operation.PLUS.eval(3, 4));
        System.out.println(Operation.MINUS.eval(5, 4));
        System.out.println(Operation.TIMES.eval(5, 4));
        System.out.println(Operation.DIVIDE.eval(5, 4));
    }
}

  编译上面程序会生成5个class文件, 其实Operation对应一个 class文件,它的4个匿名内部子类分别各对应一个class文件。
  枚举类里定义抽象方法时不能使用abstract关键字将枚举类定义 成抽象类(因为系统自动会为它添加abstract关键字), 但因为枚举 类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必 须为抽象方法提供实现,否则将出现编译错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值