01 Java 中的枚举类

Java中的枚举

​ 有时候,变量的取值只在一个有限的集合内。例如:销售的服装或比萨饼只有小、中、大和超大这四种尺寸。当然,可以将这些尺寸分别编码为 1、2、3、4 或 S、M、L、X。但这样存在着一定的隐患。在变量中很可能保存的是一个错误的值(如 0 或 m)。

​ 针对这种情况,可以自定义枚举类型。枚举类型包括有限个命名的值。

​ ——引用自《Java 核心技术卷 1 第十版》

1. 枚举的定义

在 JDK 1.5 发布时正式发布的枚举类型,在某些情景中大大的简化了我们的开发。

举一个简单的例子:星期来定义枚举类。

1.1 传统的常量定义方式

如果不使用枚举,我们可能会这样子来定义:

public class WeekConstant {
    public static final Integer WEEK_MONDAY = 1;
    public static final Integer WEEK_TUESDAY = 2;
    public static final Integer WEEK_WEDNESDAY = 3;
    public static final Integer WEEK_THURSDAY = 4;
    public static final Integer WEEK_FRIDAY = 5;
    public static final Integer WEEK_SATURDAY = 6;
    public static final Integer WEEK_SUNDAY = 7;
}

我们在使用时

@Slf4j
public class WeekTest {

    public static void main(String[] args) {
        log.info(new WeekTest().showDesc(1));
        log.info(new WeekTest().showDesc(0));
    }
    public String showDesc(int num) {
        switch (num) {
            case WeekConstant.WEEK_MONDAY:
                return "我是周一";
            case WeekConstant.WEEK_TUESDAY:
                return "我是周二";
            case WeekConstant.WEEK_WEDNESDAY:
                return "我是周三";
            case WeekConstant.WEEK_THURSDAY:
                return "我是周四";
            case WeekConstant.WEEK_FRIDAY:
                return "我是周五";
            case WeekConstant.WEEK_SATURDAY:
                return "我是周六";
            case WeekConstant.WEEK_SUNDAY:
                return "我是周日";
            default:
                return "未知";
        }
    }
}

运行起来也没问题。 但是, 我们就如同上面第二种调用方式一样, 其实我们的方向就在 7 种范围之内,但在调用的时候传入不是 1~7 之间的一个 int 类型的数据, 编译器是不会检查出来的。

1.2 枚举方法

我们现在使用枚举来实现上面的功能

定义:

public enum WeekEnum {
    /**
     * 星期
     */
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

测试:

扫描二维码关注公众号,回复: 8396686 查看本文章
@Slf4j
public class WeekTest2 {

    public static void main(String[] args) {
        log.info(new WeekTest2().showDesc(WeekEnum.SATURDAY));
//        new WeekTest2().showDesc(1); // 编译错误
    }
    public String showDesc(WeekEnum num) {
        switch (num) {
            case MONDAY:
                return "我是周一";
            case TUESDAY:
                return "我是周二";
            case WEDNESDAY:
                return "我是周三";
            case THURSDAY:
                return "我是周四";
            case FRIDAY:
                return "我是周五";
            case SATURDAY:
                return "我是周六";
            case SUNDAY:
                return "我是周日";
            default:
                return "未知";
        }
    }
}

以上只是一个举的例子, 其实, 枚举中可以很方便的获取自己的名称。

通过使用枚举, 我们可以很方便的限制了传入的参数, 如果传入的参数不是我们指定的类型, 则就发生错误。

1.3 定义总结

1.3.1相比于枚举,使用常量类的几个缺陷:

  1. 类型不安全。若一个方法中要求传入季节这个参数,用常量的话,形参就是int类型,开发者传入任意类型的int类型值就行,但是如果是枚举类型的话,就只能传入枚举类中包含的对象。
  2. 没有命名空间。假如要表达季节性的词语,开发者可能要在命名的时候以SEASON_开头,这样另外一个开发者再看这段代码的时候,才知道这四个常量分别代表季节。

1.3.2 简单小结

  • 枚举类型的定义跟类一样, 只是需要将 class 替换为 enum
  • 枚举名称与类的名称遵循一样的惯例来定义
  • 枚举值由于是常量, 一般推荐全部是大写字母
  • 多个枚举值之间使用逗号分隔开
  • 最好是在编译或设计时就知道值的所有类型, 比如上面的方向, 当然后面也可以增加

2. 枚举的本质

将上面的代码反编译,可以发现以下几个特点

2.1 继承 java.lang.Enum

通过以上的反编译, 我们知道了, java.lang.Enum是所有枚举类型的基类。查看其定义

public abstract class Enum<E extends Enum<E>>
 implements Comparable<E>, Serializable {

可以看出来, java.lang.Enum 有如下几个特征

  • 抽象类, 无法实例化
  • 实现了 Comparable 接口, 可以进行比较
  • 实现了 Serializable 接口, 可进行序列化

因此, 相对应的, 枚举类型也可以进行比较和序列化

2.2 final 类型

final 修饰, 说明枚举类型是无法进行继承的

2.3 枚举常量本身就是该类的实例对象

可以看到, 我们定义的常量, 在类内部是以实例对象存在的, 并使用静态代码块进行了实例化。

2.4 构造函数私有化

不能像正常的类一样, 从外部 new 一个对象出来。

2.5 添加了 $values[] 变量及两个方法

  • $values[]: 一个类型为枚举类本身的数组, 存储了所有的示例类型
  • values() : 获取以上所有实例变量的克隆值
  • valueOf(): 通过该方法可以通过名称获得对应的枚举常量

3. 枚举的一般使用

枚举默认是有几个方法的。

3.1 类本身的方法

类本身有两个方法,是编译时添加的

3.1.1 values()

返回的是枚举常量的克隆数组。

使用示例

final WeekEnum[] values = WeekEnum.values();
        for (WeekEnum value : values) {
            log.info(value.name());
        }

输出

15:23:05.948 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - MONDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - TUESDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - WEDNESDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - THURSDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - FRIDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - SATURDAY
15:23:05.951 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - SUNDAY

3.1.2 valueOf(String)

该方法通过字符串获取对应的枚举常量.

代码示例

final WeekEnum weekEnum = WeekEnum.valueOf("MONDAY");
log.info(weekEnum.name() + "\t" + weekEnum.ordinal());

输出

15:26:02.460 [main] INFO com.kjgym.javadavanced.meiju.WeekTest2 - MONDAY 0

3.2 继承的方法

因为枚举类型继承于 java.lang.Enum, 因此除了该类的私有方法, 其他方法都是可以使用的。

3.2.1 ordinal()

该方法返回的是枚举实例的在定义时的顺序, 类似于数组, 第一个实例该方法的返回值为 0。

在基于枚举的复杂数据结构 EnumSet和EnumMap 中会用到该函数。

log.info(WeekEnum.MONDAY.ordinal()); // 输出 0
log.info(WeekEnum.FRIDAY.ordinal()); // 输出 4

3.2.2 compareTo()

该方法时实现的 Comparable 接口的, 其实现如下。

public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
 self.getDeclaringClass() != other.getDeclaringClass())
 throw new ClassCastException();
return self.ordinal - other.ordinal;
}

首先, 需要枚举类型是同一种类型, 然后比较他们的 ordinal 来得出大于、小于还是等于。

System.out.println(WeekEnum.FRIDAY.compareTo(WeekEnum.SATURDAY));  // -1
System.out.println(WeekEnum.FRIDAY.compareTo(WeekEnum.MONDAY)); // 4

3.2.3 name() 和 toString()

该两个方法都是返回枚举常量的名称。 但是, name()方法为 final 类型, 是不能被覆盖的! 而 toString 可以被覆盖。

3.2.4 getDeclaringClass()

获取对应枚举类型的 Class 对象。

System.out.println(WeekEnum.SATURDAY.getDeclaringClass()); 

测试结果

class com.kjgym.javadavanced.meiju.WeekEnum

3.2.5 eques()

判断指定对象与枚举常量是否相同。

4. 枚举类型进阶

4.1 自定义构造函数

首先, 定义的构造函数可以是 private, 或不加修饰符

我们重新定义星期枚举类为:

public enum WeekEnum {
    /**
     * 星期
     */
    MONDAY(1, "我是周一"),
    TUESDAY(2, "我是周二"),
    WEDNESDAY(3, "我是周三"),
    THURSDAY(4, "我是周四"),
    FRIDAY(5, "我是周五"),
    SATURDAY(6, "我是周六"),
    SUNDAY(7, "我是周日");

    /**
     * 码
     */
    private int code;

    /**
     * 描述
     */
    private String desc;

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }

    Test03(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

测试

System.out.println(Test03.FRIDAY.getDesc()); // 我是周五

4.2 添加自定义的方法

4.2.1 自定义具体方法

我们在枚举类型内部加入如下具体方法

protected void show() {
    System.out.println("It is " + this.getDesc());
}

测试

Test03.SUNDAY.show();

结果

It is 我是周日

4.2.2 在枚举中定义抽象方法

@Getter
@AllArgsConstructor
@ToString
public enum Test03 {
    /**
     * 星期
     */
    MONDAY(1, "我是周一") {
        @Override
        String getName() {
            return this.name() + this.ordinal();
        }
    },
    TUESDAY(2, "我是周二") {
        @Override
        String getName() {
            return this.name() + this.ordinal();
        }
    },
    WEDNESDAY(3, "我是周三") {
        @Override
        String getName() {
            return this.name() + this.ordinal();
        }
    },
    THURSDAY(4, "我是周四") {
        @Override
        String getName() {
            return this.name() + this.ordinal();
        }
    },
    FRIDAY(5, "我是周五") {
        @Override
        String getName() {
            return this.name() + this.ordinal();
        }
    },
    SATURDAY(6, "我是周六") {
        @Override
        String getName() {
            return this.name() + this.ordinal();
        }
    },
    SUNDAY(7, "我是周日") {
        @Override
        String getName() {
            return this.name() + this.ordinal();
        }
    };

    /**
     * 码
     */
    private int code;

    /**
     * 描述
     */
    private String desc;
    
    abstract String getName();
}

简单测试

System.out.println(Test03.SUNDAY.getName());

测试结果:SUNDAY6

4.3 覆盖父类方法

在父类 java.lang.Enum 中, 也就只有 toString() 是没有使用 final 修饰啦, 要覆盖也只能覆盖该方法。

4.4 实现接口

因为Java是单继承的, 因此, Java中的枚举因为已经继承了 java.lang.Enum, 因此不能再继承其他的类。

但Java是可以实现多个接口的, 因此 Java 中的枚举也可以实现接口。

public interface Behaviour {  
    void print();  
    String getInfo();  
}  
public enum Color implements Behaviour{  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
//接口方法  
    @Override  
    public String getInfo() {  
        return this.name;  
    }  
    //接口方法  
    @Override  
    public void print() {  
        System.out.println(this.index+":"+this.name);  
    }  
}  

5. 使用枚举实现单例

该方法是在 《Effective Java》 提出的

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

单例模式的实现有很多种,网上也分析了如今实现单利模式最好用枚举,好处不外乎三点:

1.线程安全

2.不会因为序列化而产生新实例

3.防止反射攻击

但是貌似没有一篇文章解释ENUM单例如何实现了上述三点,请高手解释一下这三点:

​ 关于第一点线程安全,从反编译后的类源码中可以看出也是通过类加载机制保证的,应该是这样吧(解决)

​ 关于第二点序列化问题,有一篇文章说枚举类自己实现了readResolve()方法,所以抗序列化,这个方法是当前类自己实现的(解决)

​ 关于第三点反射攻击,我有自己试着反射攻击了以下,不过报错了...看了下方的反编译类源码,明白了,因为单例类的修饰是abstract的,所以没法实例化。(解决)

该方法无论是创建还是调用, 都是很简单。

单元素的枚举类型已经成为实现Singleton的最佳方法。

​ ——取自《Effective Java》

6. 枚举相关的集合类

猜你喜欢

转载自www.cnblogs.com/kjgym/p/12133737.html