美文网首页
Java-deep enum

Java-deep enum

作者: jiangmo | 来源:发表于2018-02-09 14:03 被阅读15次

    概念

    枚举类型(Enumerated Type) 很早就出现在编程语言中,它被用来将一组类似的值包含到一种类型当中。而这种枚举类型的名称则会被定义成独一无二的类型描述符,在这一点上和常量的定义相似。不过相比较常量类型,枚举类型可以为申明的变量提供更大的取值范围。

    用法

    常量

    public enum Color {  
      RED, GREEN, BLANK, YELLOW  
    }  
    

    switch

    enum Signal {  
        GREEN, YELLOW, RED  
    }  
    public class TrafficLight {  
        Signal color = Signal.RED;  
        public void change() {  
            switch (color) {  
            case RED:  
                color = Signal.GREEN;  
                break;  
            case YELLOW:  
                color = Signal.RED;  
                break;  
            case GREEN:  
                color = Signal.YELLOW;  
                break;  
            }  
        }  
    }  
    

    向枚举中添加新方法

    public enum Color {  
        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;  
        }  
        // 普通方法  
        public static String getName(int index) {  
            for (Color c : Color.values()) {  
                if (c.getIndex() == index) {  
                    return c.name;  
                }  
            }  
            return null;  
        }  
        // get set 方法  
        public String getName() {  
            return name;  
        }  
        public void setName(String name) {  
            this.name = name;  
        }  
        public int getIndex() {  
            return index;  
        }  
        public void setIndex(int index) {  
            this.index = index;  
        }  
    }  
    

    覆盖枚举的方法

    public enum Color {  
        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 toString() {  
            return this.index+"_"+this.name;  
        }  
    }  
    

    实现接口

    所有的枚举都继承自java.lang.Enum类。由于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);  
        }  
    }  
    

    使用接口组织枚举

    有时候,我们可能需要对一组数据进行分类,比如进行食物菜单分类而且希望这些菜单都属于food类型,appetizer(开胃菜)、mainCourse(主菜)、dessert(点心)、Coffee等,每种分类下有多种具体的菜式或食品,此时可以利用接口来组织,如下(代码引用自Thinking in Java):

    public interface Food {
      enum Appetizer implements Food {
        SALAD, SOUP, SPRING_ROLLS;
      }
      enum MainCourse implements Food {
        LASAGNE, BURRITO, PAD_THAI,
        LENTILS, HUMMOUS, VINDALOO;
      }
      enum Dessert implements Food {
        TIRAMISU, GELATO, BLACK_FOREST_CAKE,
        FRUIT, CREME_CARAMEL;
      }
      enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
        LATTE, CAPPUCCINO, TEA, HERB_TEA;
      }
    }
    
    public class TypeOfFood {
      public static void main(String[] args) {
        Food food = Appetizer.SALAD;
        food = MainCourse.LASAGNE;
        food = Dessert.GELATO;
        food = Coffee.CAPPUCCINO;
      }
    } 
    public interface Food {  
        enum Coffee implements Food{  
            BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO  
        }  
        enum Dessert implements Food{  
            FRUIT, CAKE, GELATO  
        }  
    }  
    

    枚举与单例模式

    public enum  Singleton {
        INSTANCE;
        private int id;
        private String name;
        public String getId(){
            return id;
        }
        public String getName(){
            return name;
        }
        public void setName(int id, String name){
            this.id = id;
            this.name = name;
        }
    }
    

    完全不用考虑序列化和反射的问题。枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性,这里我们不妨再次看看Enum类的valueOf方法:

    关于枚举集合的使用

    java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的key是enum类型,而value则可以是任意类型。关于这个两个集合的使用就不在这里赘述,可以参考JDK文档。

    基本原理

    定义一个Enum,通过编译之后的字节码,我们可以发现其实现原理。
    编译器是在为我们创建一个类,这个类继承自 java.lang.Enum,有两个公共的、静态的、被声明成final的属性,它们的类型就是我们定义的FruitEnum。

    编译器还生成了一个静态初始话器,就是字节码中static{};这一行下面的代码,其中的字节码创建了两个FruitEnum对象,同时分别赋值给APPLE和ORANGE这两个属性,调用的构造函数是定义在java.lang.Enum中的protected Enum(String name, int ordinal)方法。

    在创建完成两个FruitEnum对象并且分别赋值给APPLE和ORIGIN之后,还创建了一个名叫ENUM$VALUES的数组,然后把APPLE和ORIGIN按照定义的顺序放如这个数组中。

    除了这个静态初始化器之外,编译器还为我们生成了两个静态方法,values()和 valueOf(java.lang.String)方法。其中values()方法将ENUM$VALUES数组拷贝一份然后返回,而valueOf(java.lang.String)方法则会调用java.lang.Enum类中的valueOf方法,其作用是根据参数名找到对应的具体的枚举对象,如果找不到的话会抛出一个IllegalArgumentException异常。

    如:

    //定义枚举类型
    enum Day {
        MONDAY, TUESDAY, WEDNESDAY,
        THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }
    

    反编译Day.class文件:

    //反编译Day.class
    final class Day extends Enum
    {
        //编译器为我们添加的静态的values()方法
        public static Day[] values()
        {
            return (Day[])$VALUES.clone();
        }
        //编译器为我们添加的静态的valueOf()方法,注意间接调用了Enum也类的valueOf方法
        public static Day valueOf(String s)
        {
            return (Day)Enum.valueOf(com/zejian/enumdemo/Day, s);
        }
        //私有构造函数
        private Day(String s, int i)
        {
            super(s, i);
        }
         //前面定义的7种枚举实例
        public static final Day MONDAY;
        public static final Day TUESDAY;
        public static final Day WEDNESDAY;
        public static final Day THURSDAY;
        public static final Day FRIDAY;
        public static final Day SATURDAY;
        public static final Day SUNDAY;
        private static final Day $VALUES[];
    
        static 
        {    
            //实例化枚举实例
            MONDAY = new Day("MONDAY", 0);
            TUESDAY = new Day("TUESDAY", 1);
            WEDNESDAY = new Day("WEDNESDAY", 2);
            THURSDAY = new Day("THURSDAY", 3);
            FRIDAY = new Day("FRIDAY", 4);
            SATURDAY = new Day("SATURDAY", 5);
            SUNDAY = new Day("SUNDAY", 6);
            $VALUES = (new Day[] {
                MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
            });
        }
    }
    

    Enum的坑

    • enum 类型不支持 public 和 protected 修饰符的构造方法,因此构造函数一定要是 private 或 friendly 的。也正因为如此,所以枚举对象是无法在程序中通过直接调用其构造方法来初始化的。
    • 定义 enum 类型时候,如果是简单类型,那么最后一个枚举值后不用跟任何一个符号;但如果有定制方法,那么最后一个枚举值与后面代码要用分号';'隔开,不能用逗号或空格。
    • 由于 enum 类型的值实际上是通过运行期构造出对象来表示的,所以在 cluster 环境下,每个虚拟机都会构造出一个同义的枚举对象。因而在做比较操作时候就需要注意,如果直接通过使用等号 ( ‘ == ’ ) 操作符,这些看似一样的枚举值一定不相等,因为这不是同一个对象实例。

    Enum序列化反序列化原理及问题

    工具类:EnumSet、EnumMap

    EnumSet

    EnumSet 是一个针对枚举类型的高性能的 Set 接口实现。EnumSet 中装入的所有枚举对象都必须是同一种类型,在其内部,是通过 bit-vector 来实现,也就是通过一个 long 型数。EnumSet 支持在枚举类型的所有值的某个范围中进行迭代。

    理解位向量

    顾名思义位向量就是用一个bit位(0或1)标记一个元素的状态,用一组bit位表示一个集合的状态,而每个位对应一个元素,每个bit位的状态只可能有两种,即0或1。位向量能表示的元素个数与向量的bit位长度有关,如一个int类型能表示32个元素,而一个long类型则可以表示64个元素,对于EnumSet而言采用的就long类型或者long类型数组。比如现在有一个文件中的数据,该文件存储了N=1000000个无序的整数,需要把这些整数读取到内存并排序再重新写回文件中,该如何解决?最简单的方式是用int类型来存储每个数,并把其存入到数组(int a[m])中,再进行排序,但是这种方式将会导致存储空间异常大,对数据操作起来效率也能成问题,那有没更高效的方式呢?的确是有的,那就是运用位向量,我们知道一个int型的数有4个字节,也就是32位,那么我们可以用N/32个int型数组来表示这N个数:

    a[0]表示**第**1~32个数(0~31)
    a[1]表示**第**33~64个数(32~63)
    a[2]表示**第**65~96个数(64~95)
    ...... 以此类推
    

    这样,每当输入一个数字m,我们应该先找到该数字在数组的第?个元素,也就是a[?],然后再确定在这个元素的第几个bit位,找到后设置为1,代表存在该数字。举个例子来说,比如输入40,那么40/32为1余8,则应该将a[1]元素值的第9个bit位置为1(1的二进制左移8位后就是第9个位置),表示该数字存在。

    EnumMap

    代码比较简单,我们使用两种解决方案,一种是HashMap,一种EnumMap,虽然都统计出了正确的结果,但是EnumMap作为枚举的专属的集合,我们没有理由再去使用HashMap,毕竟EnumMap要求其Key必须为Enum类型,因而使用Color枚举实例作为key是最恰当不过了,也避免了获取name的步骤
    更重要的是EnumMap效率更高,因为其内部是通过数组实现的(稍后分析),注意EnumMap的key值不能为null,虽说是枚举专属集合,但其操作与一般的Map差不多,概括性来说EnumMap是专门为枚举类型量身定做的Map实现,虽然使用其它的Map(如HashMap)也能完成相同的功能,但是使用EnumMap会更加高效,它只能接收同一枚举类型的实例作为键值且不能为null,由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值,毕竟数组是一段连续的内存空间,根据程序局部性原理,效率会相当高。

    // 定义一个 EnumMap 对象,映射表主键是日期枚举类型,值是颜色枚举类型
    private static Map<WeekDayEnum, RainbowColor> schema = 
               new EnumMap<WeekDayEnum, RainbowColor>(WeekDayEnum.class); 
        
    static{ 
       // 将一周的每一天与彩虹的某一种色彩映射起来
       for (int i = 0; i < WeekDayEnum.values().length; i++) { 
           schema.put(WeekDayEnum.values()[i], RainbowColor.values()[i]); 
       } 
    } 
    System.out.println("What is the lucky color today?"); 
    System.out.println("It's " + schema.get(WeekDayEnum.Sat));
    

    Enum抽象类常见方法

    Ref:
    https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html
    http://blog.csdn.net/javazejian/article/details/71333103

    相关文章

      网友评论

          本文标题:Java-deep enum

          本文链接:https://www.haomeiwen.com/subject/zqhftftx.html