精华内容
下载资源
问答
  • 有的人说,不推荐使用枚举。有的人说,枚举很好用。究竟怎么使用,如何使用,仁者见仁智者见智。...当初的你还很年轻,不懂程序界的险恶,设计这个程序的时候,傻不拉几把这个字段设计为int类型的,

    有的人说,不推荐使用枚举。有的人说,枚举很好用。究竟怎么使用,如何使用,仁者见仁智者见智。总之,先学会再说~

    为什么要引入枚举类

    一个小案例

    你写了一个小程序,不过好久不用了,突然有一天,你想使用一下它。程序要想正确运行,需要将今天星期几存到数据库里。这个时候,你开始犯难了。

    当初的你还很年轻,不懂程序界的险恶,设计这个程序的时候,傻不拉几把这个字段设计为int类型的,用0代表周日,1代表周一。。。6代表周六,添加的时候就setWeekday(0)。但是这么长时间没用了,你忘记自己是从周一开始计算还是周日开始计算了,换句话说,你想不起来0代表的是周一还是周日了!

    于是你各种翻代码,看数据库的字段,数据库保存的信息,终于搞懂了,你很开心,用了一次之后,觉得这个程序没意思,又不用了。

    很久之后,你心血来潮,又想用一次它,很不幸,你又忘记到底0代表周一还是周日了,一番查找之后。你决定重构代码,因为你受不了了!!

    静态变量来帮忙

    经过一番思考,你决定使用七个静态变量来代表星期几,以后只要引用和静态变量就可以了,而不用自己输入012….你这么写:

    public class Weekday {
        public final static int SUN = 0;
        public final static int MON = 1;
        public final static int TUE = 2;
        public final static int WED = 3;
        public final static int THU = 4;
        public final static int FRI = 5;
        public final static int SAT = 6;
    
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    机智如你,这个时候,只要Weekday.SUN就可以了,不用操心到底应该填写0还是填写1。

    但是这个时候的你,也不是当初初出茅庐的小伙子了,很明显,这样写已经不能满足你了。你还想让这个类做更多的事,比如,你想知道下一天是星期几,还想把今天是星期几打印出来。一番深思熟虑后,你改成了这样:

    public class Weekday {
    
        private Weekday(){}
    
        public final static Weekday SUN = new Weekday();
        public final static Weekday MON = new Weekday();
        public final static Weekday TUE = new Weekday();
        public final static Weekday WED = new Weekday();
        public final static Weekday THU = new Weekday();
        public final static Weekday FRI = new Weekday();
        public final static Weekday SAT = new Weekday();
    
        public static Weekday  getNextDay(Weekday nowDay){
            if(nowDay == SUN) {
                return MON;
            }else if(nowDay == MON) {
                return TUE;
            }else if(nowDay == TUE) {
                return WED;
            }else if(nowDay == WED) {
                return THU;
            }else if(nowDay == THU) {
                return FRI;
            }else if(nowDay == FRI) {
                return SAT;
            }else {
                return SUN;
            }
        }
    
        public static void printNowDay(Weekday nowDay){
            if(nowDay == SUN)
                System.out.println("sunday");
            else if(nowDay == MON)
                System.out.println("monday");
            else if(nowDay == TUE)
                System.out.println("tuesday");
            else if(nowDay == WED)
                System.out.println("wednesday");
            else if(nowDay == THU)
                System.out.println("thursday");
            else if(nowDay == FRI)
                System.out.println("friday");
            else
                System.out.println("saturday");
        }
    
    }
    
    class Test1{
        public static void main(String[] args) {
            Weekday nowday = Weekday.SUN;
            Weekday.printNowDay(nowday);
            Weekday nextDay = Weekday.getNextDay(nowday);
            System.out.print("nextday ====> ");
            Weekday.printNowDay(nextDay);
        }
    }
    //测试结果:
    //sunday
    //nextday ====> monday
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    哟,不错。考虑的很详细。并且私有构造方法后,外界就不能创建该类的对象了,这样就避免了星期八星期九的出现,所有Weekday的对象都在该类内部创建。

    不对,好像缺了点什么,我要的是int!我的int呢?!。所以,你还需要一个这样的方法:

        public static int toInt(Weekday nowDay){
            if(nowDay == SUN)
                return 0;
            else if(nowDay == MON)
                return 1;
            else if(nowDay == TUE)
                return 2;
            else if(nowDay == WED)
                return 3;
            else if(nowDay == THU)
                return 4;
            else if(nowDay == FRI)
                return 5;
            else
               return 6;
        }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    当你需要一个整形数据的时候,只需要Weekday.toInt(Weekday.SUN);,看起来你好像完成了你的任务。

    但是,你有没有发现,这样写,好麻烦啊。如果想要扩展一下功能,大量的ifelse会让人眼花缭乱。

    有没有更好的方式呢?你大概已经知道了,没错,我们需要枚举类!

    我们先来看看枚举类是什么。

    一个简单的枚举类

    话不多说,先来代码:

    public enum Weekday {
        SUN,MON,TUS,WED,THU,FRI,SAT
    }
     
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    代码这么少?

    没错,这就是枚举类,我们来看看怎么使用它:

    class Test2{
        public static void main(String[] args) {
            Weekday sun = Weekday.SUN;
            System.out.println(sun); // 输出 SUN
        }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    看起来和上面的静态变量使用方式差不多,而且默认的toString方法返回的就是对应的名字。

    我们上面的那段代码重写toString也是不可以打印出当前是星期几的,因为toString方法没有参数。所以我们自己写了一个printNowDay方法。

    当然,这么简单的枚举类是不可能实现我们的要求的,所以,我们还要接着写:

    public enum Weekday {
        SUN(0),MON(1),TUS(2),WED(3),THU(4),FRI(5),SAT(6);
    
        private int value;
    
        private Weekday(int value){
            this.value = value;
        }
    
        public static Weekday getNextDay(Weekday nowDay){
            int nextDayValue = nowDay.value;
    
            if (++nextDayValue == 7){
                nextDayValue =0;
            }
    
            return getWeekdayByValue(nextDayValue);
        }
    
        public static Weekday getWeekdayByValue(int value) {
            for (Weekday c : Weekday.values()) {
                if (c.value == value) {
                    return c;
                }
            }
            return null;
        }
    }
    
    class Test2{
        public static void main(String[] args) {
            System.out.println("nowday ====> " + Weekday.SAT);
            System.out.println("nowday int ====> " + Weekday.SAT.ordinal());
            System.out.println("nextday ====> " + Weekday.getNextDay(Weekday.SAT)); // 输出 SUN
    
            //输出:
            //nowday ====> SAT
            //nowday int ====> 6
            //nextday ====> SUN
        }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    这样就完成了我们的目标,和之前的代码比起来,有没有觉得突然高大上了许多?没有那么多烦人的ifelse,世界都清净了。

    好了,现在你大概知道为什么要引入枚举类了吧?就是因为在没有枚举类的时候,我们要定义一个有限的序列,比如星期几,男人女人,春夏秋冬,一般会通过上面那种静态变量的形式,但是使用那样的形式如果需要一些其他的功能,需要些很多奇奇怪怪的代码。所以,枚举类的出现,就是为了简化这种操作。

    可以将枚举类理解为是Java的一种语法糖。

    枚举类的用法

    最简单的使用

    最简单的枚举类就像我们上面第一个定义的枚举类一样:

    public enum Weekday {
        SUN,MON,TUS,WED,THU,FRI,SAT
    }
     
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    如何使用它呢?

    先来看看它有哪些方法:

    Weekday方法

    这是Weekday可以调用的方法和参数。发现它有两个方法:value()和valueOf()。还有我们刚刚定义的七个变量。

    枚举变量的方法

    这些事枚举变量的方法。我们接下来会演示几个比较重要的:

    public enum Weekday {
        SUN,MON,TUS,WED,THU,FRI,SAT
    }
    
    class Test3{
        public static void main(String[] args) {
            System.out.println(Weekday.valueOf("mon".toUpperCase()));
            //MON
    
            for (Weekday w : Weekday.values()){
                System.out.println(w + ".ordinal()  ====>" +w.ordinal());
            }
            //SUN.ordinal()  ====>0
            //MON.ordinal()  ====>1
            //TUS.ordinal()  ====>2
            //WED.ordinal()  ====>3
            //THU.ordinal()  ====>4
            //FRI.ordinal()  ====>5
            //SAT.ordinal()  ====>6
    
            System.out.println("Weekday.MON.compareTo(Weekday.FRI) ===> " + Weekday.MON.compareTo(Weekday.FRI));
            System.out.println("Weekday.MON.compareTo(Weekday.MON) ===> " + Weekday.MON.compareTo(Weekday.MON));
            System.out.println("Weekday.MON.compareTo(Weekday.SUM) ===> " + Weekday.MON.compareTo(Weekday.SUN));
            //Weekday.MON.compareTo(Weekday.FRI) ===> -4
            //Weekday.MON.compareTo(Weekday.MON) ===> 0
            //Weekday.MON.compareTo(Weekday.SUM) ===> 1
    
            System.out.println("Weekday.MON.name() ====> " + Weekday.MON.name());
            //Weekday.MON.name() ====> MON
    
        }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    这段代码,我们演示了几个常用的方法和功能:

    1. Weekday.valueOf() 方法:

      它的作用是传来一个字符串,然后将它转变为对应的枚举变量。前提是你传的字符串和定义枚举变量的字符串一抹一样,区分大小写。如果你传了一个不存在的字符串,那么会抛出异常。

    2. Weekday.values()方法。

      这个方法会返回包括所有枚举变量的数组。在该例中,返回的就是包含了七个星期的Weekday[]。可以方便的用来做循环。

    3. 枚举变量的toString()方法。

      该方法直接返回枚举定义枚举变量的字符串,比如MON就返回【”MON”】。

    4. 枚举变量的.ordinal()方法。

      默认情况下,枚举类会给所有的枚举变量一个默认的次序,该次序从0开始,类似于数组的下标。而.ordinal()方法就是获取这个次序(或者说下标)

    5. 枚举变量的compareTo()方法。

      该方法用来比较两个枚举变量的”大小”,实际上比较的是两个枚举变量的次序,返回两个次序相减后的结果,如果为负数,就证明变量1”小于”变量2 (变量1.compareTo(变量2),返回【变量1.ordinal() - 变量2.ordinal()】)

      compareTo源码

      这是compareTo的源码,会先判断是不是同一个枚举类的变量,然后再返回差值。

    6. 枚举类的name()方法。

      它和toString()方法的返回值一样,事实上,这两个方法本来就是一样的: 
      name方法

      toString方法

      这两个方法的默认实现是一样的,唯一的区别是,你可以重写toString方法。name变量就是枚举变量的字符串形式。

    还有一些其他的方法我就暂时不介绍了,感兴趣的话可以自己去看看文档或者源码,都挺简单的。

    要点:

    • 使用的是enum关键字而不是class。 

    • 多个枚举变量直接用逗号隔开。 

    • 枚举变量最好大写,多个单词之间使用”_”隔开(比如:INT_SUM)。 

    • 定义完所有的变量后,以分号结束,如果只有枚举变量,而没有自定义变量,分号可以省略(例如上面的代码就忽略了分号)。 

    • 在其他类中使用enum变量的时候,只需要【类名.变量名】就可以了,和使用静态变量一样。

    但是这种简单的使用显然不能体现出枚举的强大,我们来学习一下复杂的使用:

    枚举的高级使用方法

    就像我们前面的案例一样,你需要让每一个星期几对应到一个整数,比如星期天对应0。上面讲到了,枚举类在定义的时候会自动为每个变量添加一个顺序,从0开始。

    假如你希望0代表星期天,1代表周一。。。并且你在定义枚举类的时候,顺序也是这个顺序,那你可以不用定义新的变量,就像这样:

    public enum Weekday {
        SUN,MON,TUS,WED,THU,FRI,SAT
    }
     
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    这个时候,星期天对应的ordinal值就是0,周一对应的就是1,满足你的要求。但是,如果你这么写,那就有问题了:

    public enum Weekday {
        MON,TUS,WED,THU,FRI,SAT,SUN
    }
     
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    我吧SUN放到了最后,但是我还是希0代表SUN,1代表MON怎么办呢?默认的ordinal是指望不上了,因为它只会傻傻的给第一个变量0,给第二个1。。。

    所以,我们需要自己定义变量!

    看代码:

    public enum Weekday {
        MON(1),TUS(2),WED(3),THU(4),FRI(5),SAT(6),SUN(0);
    
        private int value;
    
        private Weekday(int value){
            this.value = value;
        }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们对上面的代码做了一些改变:

    首先,我们在每个枚举变量的后面加上了一个括号,里面是我们希望它代表的数字。

    然后,我们定义了一个int变量,然后通过构造函数初始化这个变量。

    你应该也清楚了,括号里的数字,其实就是我们定义的那个int变量。这句叫做自定义变量。

    请注意:这里有三点需要注意:

    1. 一定要把枚举变量的定义放在第一行,并且以分号结尾。 

    2. 构造函数必须私有化。事实上,private是多余的,你完全没有必要写,因为它默认并强制是private,如果你要写,也只能写private,写public是不能通过编译的。 

    3. 自定义变量与默认的ordinal属性并不冲突,ordinal还是按照它的规则给每个枚举变量按顺序赋值。

    好了,你很聪明,你已经掌握了上面的知识,你想,既然能自定义一个变量,能不能自定义两个呢?

    当然可以:

    public enum Weekday {
        MON(1,"mon"),TUS(2,"tus"),WED(3,"wed"),THU(4,"thu"),FRI(5,"fri"),SAT(6,"sat"),SUN(0,"sun");
    
        private int value;
        private String label;
    
        private Weekday(int value,String label){
            this.value = value;
            this.label = label;
        }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    你可以定义任何你想要的变量。学完了这些,大概枚举类你也应该掌握了,但是,还有没有其他用法呢?

    枚举类中的抽象类

    如果我在枚举类中定义一个抽象方法会怎么样?

    你要知道,枚举类不能继承其他类,也不能被其他类继承。至于为什么,我们后面会说到。

    你应该知道,有抽象方法的类必然是抽象类,抽象类就需要子类继承它然后实现它的抽象方法,但是呢,枚举类不能被继承。。你是不是有点乱?

    我们先来看代码:

    public enum TrafficLamp {
        RED(30) {
            @Override
            public TrafficLamp getNextLamp() {
                return GREEN;
            }
        }, GREEN(45) {
            @Override
            public TrafficLamp getNextLamp() {
                return YELLOW;
            }
        }, YELLOW(5) {
            @Override
            public TrafficLamp getNextLamp() {
                return RED;
            }
        };
    
        private int time;
    
        private TrafficLamp(int time) {
            this.time = time;
        }
    
        //一个抽象方法
        public abstract TrafficLamp getNextLamp();
    
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    你好像懂了点什么。但是你好像又不太懂。为什么一个变量的后边可以带一个代码块并且实现抽象方法呢?

    别着急,带着这个疑问,我们来看一下枚举类的实现原理。

    枚举类的实现原理

    从最简单的看起:

    public enum Weekday {
        SUN,MON,TUS,WED,THU,FRI,SAT
    }
     
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    还是这段熟悉的代码,我们编译一下它,再反编译一下看看它到底是什么样子的:

    反编译代码

    你是不是觉得很熟悉?反编译出来的代码和我们一开始用静态变量自己写的那个类出奇的相似!

    而且,你看到了熟悉的values()方法和valueOf()方法。

    仔细看,这个类继承了java.lang.Enum类!所以说,枚举类不能再继承其他类了,因为默认已经继承了Enum类。

    并且,这个类是final的!所以它不能被继承!

    回到我们刚才的那个疑问:

    RED(30) {
        @Override
        public TrafficLamp getNextLamp() {
            return GREEN;
        }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    为什么会有这么神奇的代码?现在你差不多懂了。因为RED本身就是一个TrafficLamp对象的引用。实际上,在初始化这个枚举类的时候,你可以理解为执行的是TrafficLamp RED = new TrafficLamp(30) ,但是因为TrafficLamp里面有抽象方法,还记得匿名内部类么?

    我们可以这样来创建一个TrafficLamp引用:

    TrafficLamp RED = new TrafficLamp(30){
        @Override
        public TrafficLamp getNextLamp() {
            return GREEN;
        }
    };
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    而在枚举类中,我们只需要像上面那样写【RED(30){}】就可以了,因为java会自动的去帮我们完成这一系列操作。

    如果你还是不太理解,那么你可以自己去反编译一下TrafficLamp这个类,看看jvm是怎么处理它的就明白了。

    枚举类的其他用法

    说一说枚举类的其他用法。

    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;
            }
        }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    实现接口

    虽然枚举类不能继承其他类,但是还是可以实现接口的

    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);
        }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    使用接口组织枚举

    public interface Food {
        enum Coffee implements Food {
            BLACK_COFFEE, DECAF_COFFEE, LATTE, CAPPUCCINO
        }
    
        enum Dessert implements Food {
            FRUIT, CAKE, GELATO
        }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    使用枚举创建单例模式

    使用枚举创建的单例模式:

    public enum EasySingleton{
        INSTANCE;
    }
     
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    代码就这么简单,你可以使用EasySingleton.INSTANCE调用它,比起你在单例中调用getInstance()方法容易多了。

    我们来看看正常情况下是怎样创建单例模式的:

    用双检索实现单例:

    下面的代码是用双检索实现单例模式的例子,在这里getInstance()方法检查了两次来判断INSTANCE是否为null,这就是为什么叫双检索的原因,记住双检索在java5之前是有问题的,但是java5在内存模型中有了volatile变量之后就没问题了。

    public class DoubleCheckedLockingSingleton{
         private volatile DoubleCheckedLockingSingleton INSTANCE;
    
         private DoubleCheckedLockingSingleton(){}
    
         public DoubleCheckedLockingSingleton getInstance(){
             if(INSTANCE == null){
                synchronized(DoubleCheckedLockingSingleton.class){
                    //double checking Singleton instance
                    if(INSTANCE == null){
                        INSTANCE = new DoubleCheckedLockingSingleton();
                    }
                }
             }
             return INSTANCE;
         }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    你可以访问DoubleCheckedLockingSingleTon.getInstance()来获得实例对象。

    用静态工厂方法实现单例:

    public class Singleton{
        private static final Singleton INSTANCE = new Singleton();
    
        private Singleton(){}
    
        public static Singleton getSingleton(){
            return INSTANCE;
        }
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    你可以调用Singleton.getInstance()方法来获得实例对象。


    上面的两种方式就是懒汉式和恶汉式单利的创建,但是无论哪一种,都不如枚举来的方便。而且传统的单例模式的另外一个问题是一旦你实现了serializable接口,他们就不再是单例的了。但是枚举类的父类【Enum类】实现了Serializable接口,也就是说,所有的枚举类都是可以实现序列化的,这也是一个优点。


    EnumMap和EnumSet的使用:

    package com.csdn.myEnum;

     

    import java.util.EnumMap;

    import java.util.EnumSet;

     

    public class LightTest {

     

        // 1. 定义枚举类型

        public enum Light {

           // 利用构造函数传参

           RED (1), GREEN (3), YELLOW (2);

     

           // 定义私有变量

           private int nCode ;

     

           // 构造函数,枚举类型只能为私有

           private Light( int _nCode) {

               this . nCode = _nCode;

           }

     

           @Override

           public String toString() {

               return String.valueOf ( this . nCode );

           }

        }

     

        /**

          * @param args

          */

        public static void main(String[] args ) {

     

           // 1. 遍历枚举类型

           System. out .println( " 演示枚举类型的遍历 ......" );

           testTraversalEnum ();

     

           // 2. 演示 EnumMap 对象的使用

           System. out .println( " 演示 EnmuMap 对象的使用和遍历 ....." );

           testEnumMap ();

     

           // 3. 演示 EnmuSet 的使用

           System. out .println( " 演示 EnmuSet 对象的使用和遍历 ....." );

           testEnumSet ();

        }

     

        /**

          * 演示枚举类型的遍历

          */

        private static void testTraversalEnum() {

           Light[] allLight = Light.values ();

           for (Light aLight : allLight) {

               System. out .println( " 当前灯 name  " + aLight.name());

               System. out .println( " 当前灯 ordinal  " + aLight.ordinal());

               System. out .println( " 当前灯: " + aLight);

           }

        }

     

        /**

          * 演示 EnumMap 的使用, EnumMap  HashMap 的使用差不多,只不过 key 要是枚举类型

          */

        private static void testEnumMap() {

           // 1. 演示定义 EnumMap 对象, EnumMap 对象的构造函数需要参数传入 , 默认是key 的类的类型

           EnumMap<Light, String> currEnumMap = new EnumMap<Light, String>(

                  Light. class );

           currEnumMap.put(Light. RED " 红灯 " );

           currEnumMap.put(Light. GREEN " 绿灯 " );

           currEnumMap.put(Light. YELLOW " 黄灯 " );

     

           // 2. 遍历对象

           for (Light aLight : Light.values ()) {

               System. out .println( "[key=" + aLight.name() + ",value="

                      + currEnumMap.get(aLight) + "]" );

           }

        }

     

        /**

          * 演示 EnumSet 如何使用, EnumSet 是一个抽象类,获取一个类型的枚举类型内容<BR/>

          * 可以使用 allOf 方法

          */

        private static void testEnumSet() {

           EnumSet<Light> currEnumSet = EnumSet.allOf (Light. class );

           for (Light aLightSetElement : currEnumSet) {

               System. out .println( " 当前 EnumSet 中数据为: " + aLightSetElement);

           }

     

        }

    }

     

     

    执行结果如下:

    演示枚举类型的遍历 ......

    当前灯 name  RED

    当前灯 ordinal  0

    当前灯: 1

    当前灯 name  GREEN

    当前灯 ordinal  1

    当前灯: 3

    当前灯 name  YELLOW

    当前灯 ordinal  2

    当前灯: 2

    演示 EnmuMap 对象的使用和遍历 .....

    [key=RED,value= 红灯 ]

    [key=GREEN,value= 绿灯 ]

    [key=YELLOW,value= 黄灯 ]

    演示 EnmuSet 对象的使用和遍历 .....

    当前 EnumSet 中数据为: 1

    当前 EnumSet 中数据为: 3

    当前 EnumSet 中数据为: 2

     

    总结

    最后总结一下:

    • 可以创建一个enum类,把它看做一个普通的类。除了它不能继承其他类了。(java是单继承,它已经继承了Enum),可以添加其他方法,覆盖它本身的方法 

    • switch()参数可以使用enum 

    • values()方法是编译器插入到enum定义中的static方法,所以,当你将enum实例向上转型为父类Enum是,values()就不可访问了。解决办法:在Class中有一个getEnumConstants()方法,所以即便Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有的enum实例 

    • 无法从enum继承子类,如果需要扩展enum中的元素,在一个接口的内部,创建实现该接口的枚举,以此将元素进行分组。达到将枚举元素进行分组。 

    • enum允许程序员为eunm实例编写方法。所以可以为每个enum实例赋予各自不同的行为。 


    转载自:

    http://blog.csdn.net/qq_31655965/article/details/55049192

    http://blog.csdn.net/wgw335363240/article/details/6359614





    展开全文
  • java enum枚举类型 之 括号赋值

    万次阅读 2017-08-25 18:13:44
    要注意的是: 通过括号赋值,而且必须带有一个参构造器和一个属性跟方法,否则编译出错 ...按照详解,我自己写的枚举 public enum QuestionType { SINGLECHOICE(2), MULTIPLECHOICE(3), MATRIXSINGLEC

    要注意的是:

    1. 通过括号赋值,而且必须带有一个参构造器和一个属性跟方法,否则编译出错
    2. 赋值必须都赋值或都不赋值,不能一部分赋值一部分不赋值;如果不赋值则不能写构造器,赋值编译也出错
      按照详解,我自己写的枚举
    public enum QuestionType {
        SINGLECHOICE(2), 
        MULTIPLECHOICE(3), 
        MATRIXSINGLECHOICE(4), 
        MATRIXMULTIPLECHOICE(5), 
        BLANK(6), 
        MATRIXSCORING(7), 
        SCORING(50), 
        SORTING(60), 
        DESCRIPTION(70), 
        MATRIXBLANK(100), 
        MATRIXCOMBINATION(110);
    
        private final int value;
    
        // 构造器默认也只能是private, 从而保证构造函数只能在内部使用
        QuestionType(int value) {
            this.value = value;
        }
    
        public int getValue() {
            return value;
        }
    }

    详解如下:
    [http://www.cnblogs.com/linjiqin/archive/2011/02/11/1951632.html]

    展开全文
  • 从字节码指令分析Java枚举类的运行过程。 目录 一、创建的枚举类 二、反编译字节码 1)getCode()方法分析 2)setCode()方法分析 三、枚举常量给枚举类成员变量赋值过程分析 一、创建的枚举类 public enum ...

    从字节码指令分析Java枚举类的运行过程。

    目录

    一、创建的枚举类

    二、反编译字节码

    1)getCode()方法分析

    2)setCode()方法分析

    三、枚举常量给枚举类成员变量赋值过程分析


    一、创建的枚举类

    public enum ResponseCodeEnum {
        SUCCESS("0000", "success"),
        BUSINESS_EXCEPTION("5000", "wrong");
    
        private String code;
        private String message;
    
        public String getCode() {
            return code;
        }
    
        public void setCode(String code) {
            this.code = code;
        }
    
        public String getMessage() {
            return message;
        }
    
        public void setMessage(String message) {
            this.message = message;
        }
    
        ResponseCodeEnum(String code, String message) {
            this.code = code;
            this.message = message;
        }
    }

    二、反编译字节码

    1)getCode()方法分析

    字节码各指令含义在这里不再阐述,主要讲一下字节码运行之后会产生什么效果。

    了解过JVM内存模型的都知道,JVM中每一个方法都会存在一个方法栈。而字节码执行中,就会存在一个操作数栈,一个局部表量表,对应图片中stack就是操作数栈,局部变量可以在LocalVariableTable中看到;其中,Slot就是字节码中操作的一个下标。

    在大概了解了执行中存在哪些资源后,就可以开始分析了。

    首先public java.lang.String getCode()就是定义的方法声明,下面的descriptor也是描述方法的;()表示这个方法,并且没有参数,Ljava/lang/String,其中L表示这个一个引用类,后面表示这个引用类是什么类型,意思就是这个方法有返回值,返回值类型为字符串类型。

    从Code:下面开始依次分析每一行字节码含义。

    这里的stack表示操作栈的深度,为1,locals是局部变量表的变量个数,args_size表示参数个数,这里getCode()没有参数,但是显示为1,是因为每个实例方法都会有一个隐藏参数this。

    aload_0表示将Slot0这个变量压入操作栈栈顶,即将this压入栈

    getfield很好理解,就是获取this对应的code的这个变量的值,并且会把此时位于栈顶的this弹出栈。

    areturn就是返回了。局部变量表和栈都会清零。

    2)setCode()方法分析

    和getCode()相同的地方就不再阐述,这里说一下descriptor,(Ljava/lang/String;)表示这个方法有一个参数,参数为String类型,V表示该方法没有返回值,为Void

    aload_0,将这个对象this压入栈

    aload_1,将code这个参数的值压入栈,此时,code在栈顶

    putfield,将栈顶的code的值赋值给栈顶下一个this对象对应的属性,此时,this的code成员属性的值就是参数code的值了

    return。局部变量表和栈都会清零。

    三、枚举常量给枚举类成员变量赋值过程分析

    通过上面getCode()和setCode(),可以大致熟悉一下字节码指令做的事了,下面就分析,枚举常量是如何通过构造函数给枚举类成员变量赋值的。

    所有的枚举类都继承自Enum这个公共基类,在这个公共基类中,有一个构造方法,如下:

    Enum(String name, int ordinal),其中name就是我们平常写在枚举类中的常量,在这个例子中就是SUCCESS和FAIL,ordinal则是枚举常量的顺序,这里SUCCESS就是0,FAIL就是1。

    构造函数字节码如下:

    构造函数也比较简单,主要就是前面4行,前面4行就是在执行公共基类的构造函数。aload_0和aload_1会在后面讲到。

    第三行invokespecial执行的方法,通过注释可以看到那个方法是没有返回值的,参数是String和int,和公共基类的方法一致。

    接下来就是枚举常量的执行字节码:

    new,运行构造函数,创建一个枚举类实例,并压入栈顶

    dup,复制栈顶数据,并压入栈,此时操作数栈有两个值,都是new的实例

    ldc,从常量池中取出SUCCESS并压入栈

    iconst_0,将基本类型int的0压入栈

    ldc,从常量池中取出SUCCESS中的code的值(这里是0000)并压入栈

    ldc,从常量池中取出SUCCESS中的success的值(这里是success)并压入栈

    invokespecial,执行构造函数,这里通过注释,可以分析出这个方法是 method(String, int, String, String),公共基类Enum的构造函数和自定义的枚举类的构造函数的合并。

    putstatic,为枚举类的SUCCESS常量赋值。

    一个枚举类大致就会经历这几个过程,从字节码我们可以看到,枚举类的常量都是static的,虽然我们没有显示的指定。

     

    展开全文
  • 深入理解Java枚举类型(enum)

    万次阅读 多人点赞 2017-05-13 18:27:14
    【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) ... 出自【zejian的博客】 关联文章: 深入理解Java类型信息...深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解

    【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
    http://blog.csdn.net/javazejian/article/details/71333103
    出自【zejian的博客】

    关联文章:

    深入理解Java类型信息(Class对象)与反射机制

    深入理解Java枚举类型(enum)

    深入理解Java注解类型(@Annotation)

    深入理解Java并发之synchronized实现原理

    深入理解Java内存模型(JMM)及volatile关键字

    深入理解Java类加载器(ClassLoader)

    本篇主要是深入对Java中枚举类型进行分析,主要内容如下:

    理解枚举类型

    枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。下面先来看看什么是枚举?如何定义枚举?

    枚举的定义

    回忆一下下面的程序,这是在没有枚举类型时定义常量常见的方式

    /**
     * Created by zejian on 2017/5/7.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     * 使用普通方式定义日期常量
     */
    public class DayDemo {
    
        public static final int MONDAY =1;
    
        public static final int TUESDAY=2;
    
        public static final int WEDNESDAY=3;
    
        public static final int THURSDAY=4;
    
        public static final int FRIDAY=5;
    
        public static final int SATURDAY=6;
    
        public static final int SUNDAY=7;
    
    }

    上述的常量定义常量的方式称为int枚举模式,这样的定义方式并没有什么错,但它存在许多不足,如在类型安全和使用方便性上并没有多少好处,如果存在定义int值相同的变量,混淆的几率还是很大的,编译器也不会提出任何警告,因此这种方式在枚举出现后并不提倡,现在我们利用枚举类型来重新定义上述的常量,同时也感受一把枚举定义的方式,如下定义周一到周日的常量

    //枚举类型,使用关键字enum
    enum Day {
        MONDAY, TUESDAY, WEDNESDAY,
        THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }

    相当简洁,在定义枚举类型时我们使用的关键字是enum,与class关键字类似,只不过前者是定义枚举类型,后者是定义类类型。枚举类型Day中分别定义了从周一到周日的值,这里要注意,值一般是大写的字母,多个值之间以逗号分隔。同时我们应该知道的是枚举类型可以像类(class)类型一样,定义为一个单独的文件,当然也可以定义在其他类内部,更重要的是枚举常量在类型安全性和便捷性都很有保证,如果出现类型问题编译器也会提示我们改进,但务必记住枚举表示的类型其取值是必须有限的,也就是说每个值都是可以枚举出来的,比如上述描述的一周共有七天。那么该如何使用呢?如下:

    /**
     * Created by zejian on 2017/5/7.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public class EnumDemo {
    
        public static void main(String[] args){
            //直接引用
            Day day =Day.MONDAY;
        }
    
    }
    //定义枚举类型
    enum Day {
        MONDAY, TUESDAY, WEDNESDAY,
        THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }

    就像上述代码那样,直接引用枚举的值即可,这便是枚举类型的最简单模型。

    枚举实现原理

    我们大概了解了枚举类型的定义与简单使用后,现在有必要来了解一下枚举类型的基本实现原理。实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类。下面我们编译前面定义的EnumDemo.java并查看生成的class文件来验证这个结论:

    //查看目录下的java文件
    zejian@zejiandeMBP enumdemo$ ls
    EnumDemo.java
    //利用javac命令编译EnumDemo.java
    zejian@zejiandeMBP enumdemo$ javac EnumDemo.java 
    //查看生成的class文件,注意有Day.class和EnumDemo.class 两个
    zejian@zejiandeMBP enumdemo$ ls
    Day.class  EnumDemo.class  EnumDemo.java

    利用javac编译前面定义的EnumDemo.java文件后分别生成了Day.class和EnumDemo.class文件,而Day.class就是枚举类型,这也就验证前面所说的使用关键字enum定义枚举类型并编译后,编译器会自动帮助我们生成一个与枚举相关的类。我们再来看看反编译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
            });
        }
    }

    从反编译的代码可以看出编译器确实帮助我们生成了一个Day类(注意该类是final类型的,将无法被继承)而且该类继承自java.lang.Enum类,该类是一个抽象类(稍后我们会分析该类中的主要方法),除此之外,编译器还帮助我们生成了7个Day类型的实例对象分别对应枚举中定义的7个日期,这也充分说明了我们前面使用关键字enum定义的Day类型中的每种日期枚举常量也是实实在在的Day实例对象,只不过代表的内容不一样而已。注意编译器还为我们生成了两个静态方法,分别是values()和 valueOf(),稍后会分析它们的用法,到此我们也就明白了,使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象,如上述的MONDAY枚举类型对应public static final Day MONDAY;,同时编译器会为该类创建两个方法,分别是values()和valueOf()。ok~,到此相信我们对枚举的实现原理也比较清晰,下面我们深入了解一下java.lang.Enum类以及values()和valueOf()的用途。

    枚举的常见方法

    Enum抽象类常见方法

    Enum是所有 Java 语言枚举类型的公共基本类(注意Enum是抽象类),以下是它的常见方法:

    返回类型方法名称方法说明
    intcompareTo(E o)比较此枚举与指定对象的顺序
    booleanequals(Object other)当指定对象等于此枚举常量时,返回 true。
    Class<?>getDeclaringClass()返回与此枚举常量的枚举类型相对应的 Class 对象
    Stringname()返回此枚举常量的名称,在其枚举声明中对其进行声明
    intordinal()返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)
    StringtoString()返回枚举常量的名称,它包含在声明中
    static<T extends Enum<T>> Tstatic valueOf(Class<T> enumType, String name)返回带指定名称的指定枚举类型的枚举常量。

    这里主要说明一下ordinal()方法,该方法获取的是枚举变量在枚举类中声明的顺序,下标从0开始,如日期中的MONDAY在第一个位置,那么MONDAY的ordinal值就是0,如果MONDAY的声明位置发生变化,那么ordinal方法获取到的值也随之变化,注意在大多数情况下我们都不应该首先使用该方法,毕竟它总是变幻莫测的。compareTo(E o)方法则是比较枚举的大小,注意其内部实现是根据每个枚举的ordinal值大小进行比较的。name()方法与toString()几乎是等同的,都是输出变量的字符串形式。至于valueOf(Class<T> enumType, String name)方法则是根据枚举类的Class对象和枚举名称获取枚举常量,注意该方法是静态的,后面在枚举单例时,我们还会详细分析该方法,下面的代码演示了上述方法:

    package com.zejian.enumdemo;
    
    /**
     * Created by zejian on 2017/5/7.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public class EnumDemo {
    
        public static void main(String[] args){
    
            //创建枚举数组
            Day[] days=new Day[]{Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY,
                    Day.THURSDAY, Day.FRIDAY, Day.SATURDAY, Day.SUNDAY};
    
            for (int i = 0; i <days.length ; i++) {
                System.out.println("day["+i+"].ordinal():"+days[i].ordinal());
            }
    
            System.out.println("-------------------------------------");
            //通过compareTo方法比较,实际上其内部是通过ordinal()值比较的
            System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[1]));
            System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[2]));
    
            //获取该枚举对象的Class对象引用,当然也可以通过getClass方法
            Class<?> clazz = days[0].getDeclaringClass();
            System.out.println("clazz:"+clazz);
    
            System.out.println("-------------------------------------");
    
            //name()
            System.out.println("days[0].name():"+days[0].name());
            System.out.println("days[1].name():"+days[1].name());
            System.out.println("days[2].name():"+days[2].name());
            System.out.println("days[3].name():"+days[3].name());
    
            System.out.println("-------------------------------------");
    
            System.out.println("days[0].toString():"+days[0].toString());
            System.out.println("days[1].toString():"+days[1].toString());
            System.out.println("days[2].toString():"+days[2].toString());
            System.out.println("days[3].toString():"+days[3].toString());
    
            System.out.println("-------------------------------------");
    
            Day d=Enum.valueOf(Day.class,days[0].name());
            Day d2=Day.valueOf(Day.class,days[0].name());
            System.out.println("d:"+d);
            System.out.println("d2:"+d2);
        }
     /**
     执行结果:
       day[0].ordinal():0
       day[1].ordinal():1
       day[2].ordinal():2
       day[3].ordinal():3
       day[4].ordinal():4
       day[5].ordinal():5
       day[6].ordinal():6
       -------------------------------------
       days[0].compareTo(days[1]):-1
       days[0].compareTo(days[1]):-2
       clazz:class com.zejian.enumdemo.Day
       -------------------------------------
       days[0].name():MONDAY
       days[1].name():TUESDAY
       days[2].name():WEDNESDAY
       days[3].name():THURSDAY
       -------------------------------------
       days[0].toString():MONDAY
       days[1].toString():TUESDAY
       days[2].toString():WEDNESDAY
       days[3].toString():THURSDAY
       -------------------------------------
       d:MONDAY
       d2:MONDAY
       */
    
    }
    enum Day {
        MONDAY, TUESDAY, WEDNESDAY,
        THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }
    

    到此对于抽象类Enum类的基本内容就介绍完了,这里提醒大家一点,Enum类内部会有一个构造函数,该构造函数只能有编译器调用,我们是无法手动操作的,不妨看看Enum类的主要源码:

    //实现了Comparable
    public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
    
        private final String name; //枚举字符串名称
    
        public final String name() {
            return name;
        }
    
        private final int ordinal;//枚举顺序值
    
        public final int ordinal() {
            return ordinal;
        }
    
        //枚举的构造方法,只能由编译器调用
        protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
    
        public String toString() {
            return name;
        }
    
        public final boolean equals(Object other) {
            return this==other;
        }
    
        //比较的是ordinal值
        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值比较大小
        }
    
        @SuppressWarnings("unchecked")
        public final Class<E> getDeclaringClass() {
            //获取class对象引用,getClass()是Object的方法
            Class<?> clazz = getClass();
            //获取父类Class对象引用
            Class<?> zuper = clazz.getSuperclass();
            return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
        }
    
    
        public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                    String name) {
            //enumType.enumConstantDirectory()获取到的是一个map集合,key值就是name值,value则是枚举变量值   
            //enumConstantDirectory是class对象内部的方法,根据class对象获取一个map集合的值       
            T result = enumType.enumConstantDirectory().get(name);
            if (result != null)
                return result;
            if (name == null)
                throw new NullPointerException("Name is null");
            throw new IllegalArgumentException(
                "No enum constant " + enumType.getCanonicalName() + "." + name);
        }
    
        //.....省略其他没用的方法
    }

    通过Enum源码,可以知道,Enum实现了Comparable接口,这也是可以使用compareTo比较的原因,当然Enum构造函数也是存在的,该函数只能由编译器调用,毕竟我们只能使用enum关键字定义枚举,其他事情就放心交给编译器吧。

    //由编译器调用
    protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
    

    编译器生成的Values方法与ValueOf方法

    values()方法和valueOf(String name)方法是编译器生成的static方法,因此从前面的分析中,在Enum类中并没出现values()方法,但valueOf()方法还是有出现的,只不过编译器生成的valueOf()方法需传递一个name参数,而Enum自带的静态方法valueOf()则需要传递两个方法,从前面反编译后的代码可以看出,编译器生成的valueOf方法最终还是调用了Enum类的valueOf方法,下面通过代码来演示这两个方法的作用:

    Day[] days2 = Day.values();
    System.out.println("day2:"+Arrays.toString(days2));
    Day day = Day.valueOf("MONDAY");
    System.out.println("day:"+day);
    
    /**
     输出结果:
     day2:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
     day:MONDAY
     */

    从结果可知道,values()方法的作用就是获取枚举类中的所有变量,并作为数组返回,而valueOf(String name)方法与Enum类中的valueOf方法的作用类似根据名称获取枚举变量,只不过编译器生成的valueOf方法更简洁些只需传递一个参数。这里我们还必须注意到,由于values()方法是由编译器插入到枚举类中的static方法,所以如果我们将枚举实例向上转型为Enum,那么values()方法将无法被调用,因为Enum类中并没有values()方法,valueOf()方法也是同样的道理,注意是一个参数的。

     //正常使用
    Day[] ds=Day.values();
    //向上转型Enum
    Enum e = Day.MONDAY;
    //无法调用,没有此方法
    //e.values();

    枚举与Class对象

    上述我们提到当枚举实例向上转型为Enum类型后,values()方法将会失效,也就无法一次性获取所有枚举实例变量,但是由于Class对象的存在,即使不使用values()方法,还是有可能一次获取到所有枚举实例变量的,在Class对象中存在如下方法:

    返回类型方法名称方法说明
    T[]getEnumConstants()返回该枚举类型的所有元素,如果Class对象不是枚举类型,则返回null。
    booleanisEnum()当且仅当该类声明为源代码中的枚举时返回 true

    因此通过getEnumConstants()方法,同样可以轻而易举地获取所有枚举实例变量下面通过代码来演示这个功能:

    //正常使用
    Day[] ds=Day.values();
    //向上转型Enum
    Enum e = Day.MONDAY;
    //无法调用,没有此方法
    //e.values();
    //获取class对象引用
    Class<?> clasz = e.getDeclaringClass();
    if(clasz.isEnum()) {
        Day[] dsz = (Day[]) clasz.getEnumConstants();
        System.out.println("dsz:"+Arrays.toString(dsz));
    }
    
    /**
       输出结果:
       dsz:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
     */

    正如上述代码所展示,通过Enum的class对象的getEnumConstants方法,我们仍能一次性获取所有的枚举实例常量。

    枚举的进阶用法

    在前面的分析中,我们都是基于简单枚举类型的定义,也就是在定义枚举时只定义了枚举实例类型,并没定义方法或者成员变量,实际上使用关键字enum定义的枚举类,除了不能使用继承(因为编译器会自动为我们继承Enum抽象类而Java只支持单继承,因此枚举类是无法手动实现继承的),可以把enum类当成常规类,也就是说我们可以向enum类中添加方法和变量,甚至是mian方法,下面就来感受一把。

    向enum类添加方法与自定义构造函数

    重新定义一个日期枚举类,带有desc成员变量描述该日期的对于中文描述,同时定义一个getDesc方法,返回中文描述内容,自定义私有构造函数,在声明枚举实例时传入对应的中文描述,代码如下:

    package com.zejian.enumdemo;
    
    /**
     * Created by zejian on 2017/5/8.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public enum Day2 {
        MONDAY("星期一"),
        TUESDAY("星期二"),
        WEDNESDAY("星期三"),
        THURSDAY("星期四"),
        FRIDAY("星期五"),
        SATURDAY("星期六"),
        SUNDAY("星期日");//记住要用分号结束
    
        private String desc;//中文描述
    
        /**
         * 私有构造,防止被外部调用
         * @param desc
         */
        private Day2(String desc){
            this.desc=desc;
        }
    
        /**
         * 定义方法,返回描述,跟常规类的定义没区别
         * @return
         */
        public String getDesc(){
            return desc;
        }
    
        public static void main(String[] args){
            for (Day2 day:Day2.values()) {
                System.out.println("name:"+day.name()+
                        ",desc:"+day.getDesc());
            }
        }
    
        /**
         输出结果:
         name:MONDAY,desc:星期一
         name:TUESDAY,desc:星期二
         name:WEDNESDAY,desc:星期三
         name:THURSDAY,desc:星期四
         name:FRIDAY,desc:星期五
         name:SATURDAY,desc:星期六
         name:SUNDAY,desc:星期日
         */
    }

    从上述代码可知,在enum类中确实可以像定义常规类一样声明变量或者成员方法。但是我们必须注意到,如果打算在enum类中定义方法,务必在声明完枚举实例后使用分号分开,倘若在枚举实例前定义任何方法,编译器都将会报错,无法编译通过,同时即使自定义了构造函数且enum的定义结束,我们也永远无法手动调用构造函数创建枚举实例,毕竟这事只能由编译器执行。

    关于覆盖enum类方法

    既然enum类跟常规类的定义没什么区别(实际上enum还是有些约束的),那么覆盖父类的方法也不会是什么难说,可惜的是父类Enum中的定义的方法只有toString方法没有使用final修饰,因此只能覆盖toString方法,如下通过覆盖toString省去了getDesc方法:

    package com.zejian.enumdemo;
    
    /**
     * Created by zejian on 2017/5/8.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public enum Day2 {
        MONDAY("星期一"),
        TUESDAY("星期二"),
        WEDNESDAY("星期三"),
        THURSDAY("星期四"),
        FRIDAY("星期五"),
        SATURDAY("星期六"),
        SUNDAY("星期日");//记住要用分号结束
    
        private String desc;//中文描述
    
        /**
         * 私有构造,防止被外部调用
         * @param desc
         */
        private Day2(String desc){
            this.desc=desc;
        }
    
        /**
         * 覆盖
         * @return
         */
        @Override
        public String toString() {
            return desc;
        }
    
    
        public static void main(String[] args){
            for (Day2 day:Day2.values()) {
                System.out.println("name:"+day.name()+
                        ",desc:"+day.toString());
            }
        }
    
        /**
         输出结果:
         name:MONDAY,desc:星期一
         name:TUESDAY,desc:星期二
         name:WEDNESDAY,desc:星期三
         name:THURSDAY,desc:星期四
         name:FRIDAY,desc:星期五
         name:SATURDAY,desc:星期六
         name:SUNDAY,desc:星期日
         */
    }

    enum类中定义抽象方法

    与常规抽象类一样,enum类允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,以便产生不同的行为方式,注意abstract关键字对于枚举类来说并不是必须的如下:

    package com.zejian.enumdemo;
    
    /**
     * Created by zejian on 2017/5/9.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public enum EnumDemo3 {
    
        FIRST{
            @Override
            public String getInfo() {
                return "FIRST TIME";
            }
        },
        SECOND{
            @Override
            public String getInfo() {
                return "SECOND TIME";
            }
        }
    
        ;
    
        /**
         * 定义抽象方法
         * @return
         */
        public abstract String getInfo();
    
        //测试
        public static void main(String[] args){
            System.out.println("F:"+EnumDemo3.FIRST.getInfo());
            System.out.println("S:"+EnumDemo3.SECOND.getInfo());
            /**
             输出结果:
             F:FIRST TIME
             S:SECOND TIME
             */
        }
    }

    通过这种方式就可以轻而易举地定义每个枚举实例的不同行为方式。我们可能注意到,enum类的实例似乎表现出了多态的特性,可惜的是枚举类型的实例终究不能作为类型传递使用,就像下面的使用方式,编译器是不可能答应的:

    //无法通过编译,毕竟EnumDemo3.FIRST是个实例对象
     public void text(EnumDemo3.FIRST instance){ }

    在枚举实例常量中定义抽象方法

    enum类与接口

    由于Java单继承的原因,enum类并不能再继承其它类,但并不妨碍它实现接口,因此enum类同样是可以实现多接口的,如下:

    package com.zejian.enumdemo;
    
    /**
     * Created by zejian on 2017/5/8.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    
    interface food{
        void eat();
    }
    
    interface sport{
        void run();
    }
    
    public enum EnumDemo2 implements food ,sport{
        FOOD,
        SPORT,
        ; //分号分隔
    
        @Override
        public void eat() {
            System.out.println("eat.....");
        }
    
        @Override
        public void run() {
            System.out.println("run.....");
        }
    }

    有时候,我们可能需要对一组数据进行分类,比如进行食物菜单分类而且希望这些菜单都属于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;
      }
    } 

    通过这种方式可以很方便组织上述的情景,同时确保每种具体类型的食物也属于Food,现在我们利用一个枚举嵌套枚举的方式,把前面定义的菜谱存放到一个Meal菜单中,通过这种方式就可以统一管理菜单的数据了。

    public enum Meal{
      APPETIZER(Food.Appetizer.class),
      MAINCOURSE(Food.MainCourse.class),
      DESSERT(Food.Dessert.class),
      COFFEE(Food.Coffee.class);
      private Food[] values;
      private Meal(Class<? extends Food> kind) {
        //通过class对象获取枚举实例
        values = kind.getEnumConstants();
      }
      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;
        }
      }
    } 

    枚举与switch

    关于枚举与switch是个比较简单的话题,使用switch进行条件判断时,条件参数一般只能是整型,字符型。而枚举型确实也被switch所支持,在java 1.7后switch也对字符串进行了支持。这里我们简单看一下switch与枚举类型的使用:

    
    /**
     * Created by zejian on 2017/5/9.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    
    enum Color {GREEN,RED,BLUE}
    
    public class EnumDemo4 {
    
        public static void printName(Color color){
            switch (color){
                case BLUE: //无需使用Color进行引用
                    System.out.println("蓝色");
                    break;
                case RED:
                    System.out.println("红色");
                    break;
                case GREEN:
                    System.out.println("绿色");
                    break;
            }
        }
    
        public static void main(String[] args){
            printName(Color.BLUE);
            printName(Color.RED);
            printName(Color.GREEN);
    
            //蓝色
            //红色
            //绿色
        }
    }

    需要注意的是使用在于switch条件进行结合使用时,无需使用Color引用。

    枚举与单例模式

    单例模式可以说是最常使用的设计模式了,它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供这个实例。在实际应用中,线程池、缓存、日志对象、对话框对象常被设计成单例,总之,选择单例模式就是为了避免不一致状态,下面我们将会简单说明单例模式的几种主要编写方式,从而对比出使用枚举实现单例模式的优点。首先看看饿汉式的单例模式:

    /**
     * Created by wuzejian on 2017/5/9.
     * 饿汉式(基于classloder机制避免了多线程的同步问题)
     */
    public class SingletonHungry {
    
        private static SingletonHungry instance = new SingletonHungry();
    
        private SingletonHungry() {
        }
    
        public static SingletonHungry getInstance() {
            return instance;
        }
    }

    显然这种写法比较简单,但问题是无法做到延迟创建对象,事实上如果该单例类涉及资源较多,创建比较耗时间时,我们更希望它可以尽可能地延迟加载,从而减小初始化的负载,于是便有了如下的懒汉式单例:

    /**
     * Created by wuzejian on 2017/5/9..
     * 懒汉式单例模式(适合多线程安全)
     */
    public class SingletonLazy {
    
        private static volatile SingletonLazy instance;
    
        private SingletonLazy() {
        }
    
        public static synchronized SingletonLazy getInstance() {
            if (instance == null) {
                instance = new SingletonLazy();
            }
            return instance;
        }
    }

    这种写法能够在多线程中很好的工作避免同步问题,同时也具备lazy loading机制,遗憾的是,由于synchronized的存在,效率很低,在单线程的情景下,完全可以去掉synchronized,为了兼顾效率与性能问题,改进后代码如下:

    public class Singleton {
        private static volatile Singleton singleton = null;
    
        private Singleton(){}
    
        public static Singleton getSingleton(){
            if(singleton == null){
                synchronized (Singleton.class){
                    if(singleton == null){
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }    
    }

    这种编写方式被称为“双重检查锁”,主要在getSingleton()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。但是必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。或许我们可以利用静态内部类来实现更安全的机制,静态内部类单例模式如下:

    /**
     * Created by wuzejian on 2017/5/9.
     * 静态内部类
     */
    public class SingletonInner {
        private static class Holder {
            private static SingletonInner singleton = new SingletonInner();
        }
    
        private SingletonInner(){}
    
        public static SingletonInner getSingleton(){
            return Holder.singleton;
        }
    }

    正如上述代码所展示的,我们把Singleton实例放到一个静态内部类中,这样可以避免了静态实例在Singleton类的加载阶段(类加载过程的其中一个阶段的,此时只创建了Class对象,关于Class对象可以看博主另外一篇博文, 深入理解Java类型信息(Class对象)与反射机制)就创建对象,毕竟静态变量初始化是在SingletonInner类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。从上述4种单例模式的写法中,似乎也解决了效率与懒加载的问题,但是它们都有两个共同的缺点:

    • 序列化可能会破坏单例模式,比较每次反序列化一个序列化的对象实例时都会创建一个新的实例,解决方案如下:

      //测试例子(四种写解决方式雷同)
      public class Singleton implements java.io.Serializable {     
         public static Singleton INSTANCE = new Singleton();     
      
         protected Singleton() {     
         }  
      
         //反序列时直接返回当前INSTANCE
         private Object readResolve() {     
                  return INSTANCE;     
            }    
      }   
    • 使用反射强行调用私有构造器,解决方式可以修改构造器,让它在创建第二个实例的时候抛异常,如下:

      public static Singleton INSTANCE = new Singleton();     
      private static volatile  boolean  flag = true;
      private Singleton(){
          if(flag){
          flag = false;   
          }else{
              throw new RuntimeException("The instance  already exists !");
          }
      }

    如上所述,问题确实也得到了解决,但问题是我们为此付出了不少努力,即添加了不少代码,还应该注意到如果单例类维持了其他对象的状态时还需要使他们成为transient的对象,这种就更复杂了,那有没有更简单更高效的呢?当然是有的,那就是枚举单例了,先来看看如何实现:

    /**
     * Created by wuzejian on 2017/5/9.
     * 枚举单利
     */
    public enum  SingletonEnum {
        INSTANCE;
        private String name;
        public String getName(){
            return name;
        }
        public void setName(String name){
            this.name = name;
        }
    }

    代码相当简洁,我们也可以像常规类一样编写enum类,为其添加变量和方法,访问方式也更简单,使用SingletonEnum.INSTANCE进行访问,这样也就避免调用getInstance方法,更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射的问题。枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性,这里我们不妨再次看看Enum类的valueOf方法:

    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                  String name) {
          T result = enumType.enumConstantDirectory().get(name);
          if (result != null)
              return result;
          if (name == null)
              throw new NullPointerException("Name is null");
          throw new IllegalArgumentException(
              "No enum constant " + enumType.getCanonicalName() + "." + name);
      }

    实际上通过调用enumType(Class对象的引用)的enumConstantDirectory方法获取到的是一个Map集合,在该集合中存放了以枚举name为key和以枚举实例变量为value的Key&Value数据,因此通过name的值就可以获取到枚举实例,看看enumConstantDirectory方法源码:

    Map<String, T> enumConstantDirectory() {
            if (enumConstantDirectory == null) {
                //getEnumConstantsShared最终通过反射调用枚举类的values方法
                T[] universe = getEnumConstantsShared();
                if (universe == null)
                    throw new IllegalArgumentException(
                        getName() + " is not an enum type");
                Map<String, T> m = new HashMap<>(2 * universe.length);
                //map存放了当前enum类的所有枚举实例变量,以name为key值
                for (T constant : universe)
                    m.put(((Enum<?>)constant).name(), constant);
                enumConstantDirectory = m;
            }
            return enumConstantDirectory;
        }
        private volatile transient Map<String, T> enumConstantDirectory = null;

    到这里我们也就可以看出枚举序列化确实不会重新创建新实例,jvm保证了每个枚举实例变量的唯一性。再来看看反射到底能不能创建枚举,下面试图通过反射获取构造器并创建枚举

    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
      //获取枚举类的构造函数(前面的源码已分析过)
       Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
       constructor.setAccessible(true);
       //创建枚举
       SingletonEnum singleton=constructor.newInstance("otherInstance",9);
      }

    执行报错

    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
        at zejian.SingletonEnum.main(SingletonEnum.java:38)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

    显然告诉我们不能使用反射创建枚举类,这是为什么呢?不妨看看newInstance方法源码:

     public T newInstance(Object ... initargs)
            throws InstantiationException, IllegalAccessException,
                   IllegalArgumentException, InvocationTargetException
        {
            if (!override) {
                if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                    Class<?> caller = Reflection.getCallerClass();
                    checkAccess(caller, clazz, null, modifiers);
                }
            }
            //这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
            if ((clazz.getModifiers() & Modifier.ENUM) != 0)
                throw new IllegalArgumentException("Cannot reflectively create enum objects");
            ConstructorAccessor ca = constructorAccessor;   // read volatile
            if (ca == null) {
                ca = acquireConstructorAccessor();
            }
            @SuppressWarnings("unchecked")
            T inst = (T) ca.newInstance(initargs);
            return inst;
        }

    源码很了然,确实无法使用反射创建枚举实例,也就是说明了创建枚举实例只有编译器能够做到而已。显然枚举单例模式确实是很不错的选择,因此我们推荐使用它。但是这总不是万能的,对于android平台这个可能未必是最好的选择,在android开发中,内存优化是个大块头,而使用枚举时占用的内存常常是静态变量的两倍还多,因此android官方在内存优化方面给出的建议是尽量避免在android中使用enum。但是不管如何,关于单例,我们总是应该记住:线程安全,延迟加载,序列化与反序列化安全,反射安全是很重重要的。

    EnumMap

    EnumMap基本用法

    先思考这样一个问题,现在我们有一堆size大小相同而颜色不同的数据,需要统计出每种颜色的数量是多少以便将数据录入仓库,定义如下枚举用于表示颜色Color:

    enum Color {
        GREEN,RED,BLUE,YELLOW
    }

    我们有如下解决方案,使用Map集合来统计,key值作为颜色名称,value代表衣服数量,如下:

    package com.zejian.enumdemo;
    
    import java.util.*;
    
    /**
     * Created by zejian on 2017/5/10.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public class EnumMapDemo {
        public static void main(String[] args){
            List<Clothes> list = new ArrayList<>();
            list.add(new Clothes("C001",Color.BLUE));
            list.add(new Clothes("C002",Color.YELLOW));
            list.add(new Clothes("C003",Color.RED));
            list.add(new Clothes("C004",Color.GREEN));
            list.add(new Clothes("C005",Color.BLUE));
            list.add(new Clothes("C006",Color.BLUE));
            list.add(new Clothes("C007",Color.RED));
            list.add(new Clothes("C008",Color.YELLOW));
            list.add(new Clothes("C009",Color.YELLOW));
            list.add(new Clothes("C010",Color.GREEN));
            //方案1:使用HashMap
            Map<String,Integer> map = new HashMap<>();
            for (Clothes clothes:list){
               String colorName=clothes.getColor().name();
               Integer count = map.get(colorName);
                if(count!=null){
                    map.put(colorName,count+1);
                }else {
                    map.put(colorName,1);
                }
            }
    
            System.out.println(map.toString());
    
            System.out.println("---------------");
    
            //方案2:使用EnumMap
            Map<Color,Integer> enumMap=new EnumMap<>(Color.class);
    
            for (Clothes clothes:list){
                Color color=clothes.getColor();
                Integer count = enumMap.get(color);
                if(count!=null){
                    enumMap.put(color,count+1);
                }else {
                    enumMap.put(color,1);
                }
            }
    
            System.out.println(enumMap.toString());
        }
    
        /**
         输出结果:
         {RED=2, BLUE=3, YELLOW=3, GREEN=2}
         ---------------
         {GREEN=2, RED=2, BLUE=3, YELLOW=3}
         */
    }

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

    //创建一个具有指定键类型的空枚举映射。
    EnumMap(Class<K> keyType) 
    //创建一个其键类型与指定枚举映射相同的枚举映射,最初包含相同的映射关系(如果有的话)。     
    EnumMap(EnumMap<K,? extends V> m) 
    //创建一个枚举映射,从指定映射对其初始化。
    EnumMap(Map<K,? extends V> m)       

    与HashMap不同,它需要传递一个类型信息,即Class对象,通过这个参数EnumMap就可以根据类型信息初始化其内部数据结构,另外两只是初始化时传入一个Map集合,代码演示如下:

    //使用第一种构造
    Map<Color,Integer> enumMap=new EnumMap<>(Color.class);
    //使用第二种构造
    Map<Color,Integer> enumMap2=new EnumMap<>(enumMap);
    //使用第三种构造
    Map<Color,Integer> hashMap = new HashMap<>();
    hashMap.put(Color.GREEN, 2);
    hashMap.put(Color.BLUE, 3);
    Map<Color, Integer> enumMap = new EnumMap<>(hashMap);

    至于EnumMap的方法,跟普通的map几乎没有区别,注意与HashMap的主要不同在于构造方法需要传递类型参数和EnumMap保证Key顺序与枚举中的顺序一致,但请记住Key不能为null。

    EnumMap实现原理剖析

    EnumMap的源码有700多行,这里我们主要分析其内部存储结构,添加查找的实现,了解这几点,对应EnumMap内部实现原理也就比较清晰了,先看数据结构和构造函数

    public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
        implements java.io.Serializable, Cloneable
    {
        //Class对象引用
        private final Class<K> keyType;
    
        //存储Key值的数组
        private transient K[] keyUniverse;
    
        //存储Value值的数组
        private transient Object[] vals;
    
        //map的size
        private transient int size = 0;
    
        //空map
        private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0];
    
        //构造函数
        public EnumMap(Class<K> keyType) {
            this.keyType = keyType;
            keyUniverse = getKeyUniverse(keyType);
            vals = new Object[keyUniverse.length];
        }
    
    }

    EnumMap继承了AbstractMap类,因此EnumMap具备一般map的使用方法,keyType表示类型信息,keyUniverse表示键数组,存储的是所有可能的枚举值,vals数组表示键对应的值,size表示键值对个数。在构造函数中通过keyUniverse = getKeyUniverse(keyType);初始化了keyUniverse数组的值,内部存储的是所有可能的枚举值,接着初始化了存在Value值得数组vals,其大小与枚举实例的个数相同,getKeyUniverse方法实现如下

    //返回枚举数组
    private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
            //最终调用到枚举类型的values方法,values方法返回所有可能的枚举值
            return SharedSecrets.getJavaLangAccess()
                                            .getEnumConstantsShared(keyType);
        }

    从方法的返回值来看,返回类型是枚举数组,事实也是如此,最终返回值正是枚举类型的values方法的返回值,前面我们分析过values方法返回所有可能的枚举值,因此keyUniverse数组存储就是枚举类型的所有可能的枚举值。接着看put方法的实现

     public V put(K key, V value) {
            typeCheck(key);//检测key的类型
            //获取存放value值得数组下标
            int index = key.ordinal();
            //获取旧值
            Object oldValue = vals[index];
            //设置value值
            vals[index] = maskNull(value);
            if (oldValue == null)
                size++;
            return unmaskNull(oldValue);//返回旧值
        }

    这里通过typeCheck方法进行了key类型检测,判断是否为枚举类型,如果类型不对,会抛出异常

    private void typeCheck(K key) {
       Class<?> keyClass = key.getClass();//获取类型信息
       if (keyClass != keyType && keyClass.getSuperclass() != keyType)
           throw new ClassCastException(keyClass + " != " + keyType);
    }

    接着通过int index = key.ordinal()的方式获取到该枚举实例的顺序值,利用此值作为下标,把值存储在vals数组对应下标的元素中即vals[index],这也是为什么EnumMap能维持与枚举实例相同存储顺序的原因,我们发现在对vals[]中元素进行赋值和返回旧值时分别调用了maskNull方法和unmaskNull方法

     //代表NULL值得空对象实例
      private static final Object NULL = new Object() {
            public int hashCode() {
                return 0;
            }
    
            public String toString() {
                return "java.util.EnumMap.NULL";
            }
        };
    
        private Object maskNull(Object value) {
            //如果值为空,返回NULL对象,否则返回value
            return (value == null ? NULL : value);
        }
    
        @SuppressWarnings("unchecked")
        private V unmaskNull(Object value) {
            //将NULL对象转换为null值
            return (V)(value == NULL ? null : value);
        }

    由此看来EnumMap还是允许存放null值的,但key绝对不能为null,对于null值,EnumMap进行了特殊处理,将其包装为NULL对象,毕竟vals[]存的是Object,maskNull方法和unmaskNull方法正是用于null的包装和解包装的。这就是EnumMap集合的添加过程。下面接着看获取方法

     public V get(Object key) {
            return (isValidKey(key) ?
                    unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
        }
    
     //对Key值的有效性和类型信息进行判断
     private boolean isValidKey(Object key) {
          if (key == null)
              return false;
    
          // Cheaper than instanceof Enum followed by getDeclaringClass
          Class<?> keyClass = key.getClass();
          return keyClass == keyType || keyClass.getSuperclass() == keyType;
      }

    相对应put方法,get方法显示相当简洁,key有效的话,直接通过ordinal方法取索引,然后在值数组vals里通过索引获取值返回。remove方法如下:

     public V remove(Object key) {
            //判断key值是否有效
            if (!isValidKey(key))
                return null;
            //直接获取索引
            int index = ((Enum<?>)key).ordinal();
    
            Object oldValue = vals[index];
            //对应下标元素值设置为null
            vals[index] = null;
            if (oldValue != null)
                size--;//减size
            return unmaskNull(oldValue);
        }

    非常简单,key值有效,通过key获取下标索引值,把vals[]对应下标值设置为null,size减一。查看是否包含某个值,

    判断是否包含某value
    public boolean containsValue(Object value) {
        value = maskNull(value);
        //遍历数组实现
        for (Object val : vals)
            if (value.equals(val))
                return true;
    
        return false;
    }
    //判断是否包含key
    public boolean containsKey(Object key) {
        return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null;
    }

    判断value直接通过遍历数组实现,而判断key就更简单了,判断key是否有效和对应vals[]中是否存在该值。ok~,这就是EnumMap的主要实现原理,即内部有两个数组,长度相同,一个表示所有可能的键(枚举值),一个表示对应的值,不允许keynull,但允许value为null,键都有一个对应的索引,根据索引直接访问和操作其键数组和值数组,由于操作都是数组,因此效率很高。

    EnumSet

    EnumSet是与枚举类型一起使用的专用 Set 集合,EnumSet 中所有元素都必须是枚举类型。与其他Set接口的实现类HashSet/TreeSet(内部都是用对应的HashMap/TreeMap实现的)不同的是,EnumSet在内部实现是位向量(稍后分析),它是一种极为高效的位运算操作,由于直接存储和操作都是bit,因此EnumSet空间和时间性能都十分可观,足以媲美传统上基于 int 的“位标志”的运算,重要的是我们可像操作set集合一般来操作位运算,这样使用代码更简单易懂同时又具备类型安全的优势。注意EnumSet不允许使用 null 元素。试图插入 null 元素将抛出 NullPointerException,但试图测试判断是否存在null 元素或移除 null 元素则不会抛出异常,与大多数collection 实现一样,EnumSet不是线程安全的,因此在多线程环境下应该注意数据同步问题,ok~,下面先来简单看看EnumSet的使用方式。

    EnumSet用法

    创建EnumSet并不能使用new关键字,因为它是个抽象类,而应该使用其提供的静态工厂方法,EnumSet的静态工厂方法比较多,如下:

    创建一个具有指定元素类型的空EnumSet。
    EnumSet<E>  noneOf(Class<E> elementType)       
    //创建一个指定元素类型并包含所有枚举值的EnumSet
    <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType)
    // 创建一个包括枚举值中指定范围元素的EnumSet
    <E extends Enum<E>> EnumSet<E> range(E from, E to)
    // 初始集合包括指定集合的补集
    <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)
    // 创建一个包括参数中所有元素的EnumSet
    <E extends Enum<E>> EnumSet<E> of(E e)
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2)
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
    <E extends Enum<E>> EnumSet<E> of(E first, E... rest)
    //创建一个包含参数容器中的所有元素的EnumSet
    <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s)
    <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)

    代码演示如下:

    package zejian;
    
    import java.util.ArrayList;
    import java.util.EnumSet;
    import java.util.List;
    
    /**
     * Created by wuzejian on 2017/5/12.
     *
     */
    enum Color {
        GREEN , RED , BLUE , BLACK , YELLOW
    }
    
    
    public class EnumSetDemo {
    
        public static void main(String[] args){
    
            //空集合
            EnumSet<Color> enumSet= EnumSet.noneOf(Color.class);
            System.out.println("添加前:"+enumSet.toString());
            enumSet.add(Color.GREEN);
            enumSet.add(Color.RED);
            enumSet.add(Color.BLACK);
            enumSet.add(Color.BLUE);
            enumSet.add(Color.YELLOW);
            System.out.println("添加后:"+enumSet.toString());
    
            System.out.println("-----------------------------------");
    
            //使用allOf创建包含所有枚举类型的enumSet,其内部根据Class对象初始化了所有枚举实例
            EnumSet<Color> enumSet1= EnumSet.allOf(Color.class);
            System.out.println("allOf直接填充:"+enumSet1.toString());
    
            System.out.println("-----------------------------------");
    
            //初始集合包括枚举值中指定范围的元素
            EnumSet<Color> enumSet2= EnumSet.range(Color.BLACK,Color.YELLOW);
            System.out.println("指定初始化范围:"+enumSet2.toString());
    
            System.out.println("-----------------------------------");
    
            //指定补集,也就是从全部枚举类型中去除参数集合中的元素,如下去掉上述enumSet2的元素
            EnumSet<Color> enumSet3= EnumSet.complementOf(enumSet2);
            System.out.println("指定补集:"+enumSet3.toString());
    
            System.out.println("-----------------------------------");
    
            //初始化时直接指定元素
            EnumSet<Color> enumSet4= EnumSet.of(Color.BLACK);
            System.out.println("指定Color.BLACK元素:"+enumSet4.toString());
            EnumSet<Color> enumSet5= EnumSet.of(Color.BLACK,Color.GREEN);
            System.out.println("指定Color.BLACK和Color.GREEN元素:"+enumSet5.toString());
    
            System.out.println("-----------------------------------");
    
            //复制enumSet5容器的数据作为初始化数据
            EnumSet<Color> enumSet6= EnumSet.copyOf(enumSet5);
            System.out.println("enumSet6:"+enumSet6.toString());
    
            System.out.println("-----------------------------------");
    
            List<Color> list = new ArrayList<Color>();
            list.add(Color.BLACK);
            list.add(Color.BLACK);//重复元素
            list.add(Color.RED);
            list.add(Color.BLUE);
            System.out.println("list:"+list.toString());
    
            //使用copyOf(Collection<E> c)
            EnumSet enumSet7=EnumSet.copyOf(list);
            System.out.println("enumSet7:"+enumSet7.toString());
    
            /**
             输出结果:
             添加前:[]
             添加后:[GREEN, RED, BLUE, BLACK, YELLOW]
             -----------------------------------
             allOf直接填充:[GREEN, RED, BLUE, BLACK, YELLOW]
             -----------------------------------
             指定初始化范围:[BLACK, YELLOW]
             -----------------------------------
             指定补集:[GREEN, RED, BLUE]
             -----------------------------------
             指定Color.BLACK元素:[BLACK]
             指定Color.BLACK和Color.GREEN元素:[GREEN, BLACK]
             -----------------------------------
             enumSet6:[GREEN, BLACK]
             -----------------------------------
             list:[BLACK, BLACK, RED, BLUE]
             enumSet7:[RED, BLUE, BLACK]
             */
        }
    
    }
    

    noneOf(Class<E> elementType)静态方法,主要用于创建一个空的EnumSet集合,传递参数elementType代表的是枚举类型的类型信息,即Class对象。EnumSet<E> allOf(Class<E> elementType)静态方法则是创建一个填充了elementType类型所代表的所有枚举实例,奇怪的是EnumSet提供了多个重载形式的of方法,最后一个接受的的是可变参数,其他重载方法则是固定参数个数,EnumSet之所以这样设计是因为可变参数的运行效率低一些,所有在参数数据不多的情况下,强烈不建议使用传递参数为可变参数的of方法,即EnumSet<E> of(E first, E... rest),其他方法就不分析了,看代码演示即可。至于EnumSet的操作方法,则与set集合是一样的,可以看API即可这也不过多说明。什么时候使用EnumSet比较恰当的,事实上当需要进行位域运算,就可以使用EnumSet提到位域,如下:

    public class EnumSetDemo {
        //定义位域变量
        public static final int TYPE_ONE = 1 << 0 ; //1
        public static final int TYPE_TWO = 1 << 1 ; //2
        public static final int TYPE_THREE = 1 << 2 ; //4
        public static final int TYPE_FOUR = 1 << 3 ; //8
        public static void main(String[] args){
            //位域运算
            int type= TYPE_ONE | TYPE_TWO | TYPE_THREE |TYPE_FOUR;
        }
    }

    诸如上述情况,我们都可以将上述的类型定义成枚举然后采用EnumSet来装载,进行各种操作,这样不仅不用手动编写太多冗余代码,而且使用EnumSet集合进行操作也将使代码更加简洁明了。

    enum Type{
        TYPE_ONE,TYPE_TWO,TYPE_THREE,TYPE_FOUR 
    }
    
    public class EnumSetDemo {
        public static void main(String[] args){
        EnumSet set =EnumSet.of(Type.TYPE_ONE,Type.TYPE_FOUR);
        }
    }

    其实博主认为EnumSet最有价值的是其内部实现原理,采用的是位向量,它体现出来的是一种高效的数据处理方式,这点很值得我们去学习它。

    EnumSet实现原理剖析

    关于EnumSet实现原理可能会有点烧脑,内部执行几乎都是位运算,博主将尽力使用图片来分析,协助大家理解。

    理解位向量

    在分析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个位置),表示该数字存在,40数字的表示原理图过程如下:

    大概明白了位向量表示方式后,上述过程的计算方式,通过以下方式可以计算该数存储在数组的第?个元素和元素中第?个bit位置,为了演示方便,我们这里假设整第?个元素中的?为P,余值设置S

    
    //m 除以 2^n 则商(P)表示为 m >> n 
    //等同于 m / 2^5 取整数 即:40 / 32 = 1 ,那么P=1就是数组中第2个元素,即a[1]
    
    //位操作过程如下,40的二进制
    00000000 00000000 00000000 00101000
    
    //右移5位即 n=5 , m >> 5 ,即结果转为10进制就是P=1
    00000000 00000000 00000000 00000001

    在这里我们使用的int类型,即32位,所有2^5=32,因此n=5,由此计算出 P的值代表的是数组的第 P 个元素,接着利用下述方式计算出余数(S),以此设置该元素值的第(S+1)个bit位为1

    //m 除以2^n 的余数(S)表示为 m & (2^n-1) 
    //等同于: m % 2^5 取余数 即:40 % 32 = 8
    
    //m=40的二进制
    00000000 00000000 00000000 00101000
    
    //2^n-1(31)的二进制
    00000000 00000000 00000000 00011111
    
    // m & (2^n-1) 即40与31进行与操作得出余数 即 S=8
    00000000 00000000 00000000 00001000 
    
    //下面是将a[1]元素值的第(8+1)个bit设置为1,为什么是(8+1)不是8?因为1左移8位就在第9个bit位了,过程如下:
    
    //1的二进制如下:
    00000000 00000000 00000000 00000001
    
    //1 << 8 利用余数8对1进行左移动
    00000000 00000000 00000001 0000000 
    
    //然后再与a[1]执行或操作后就可以将对应的bit位设置为1
    //a[P] |= 1 << S 见下述java实现的代码

    通过上述二进制位运算过程(关于位运算可以看博主的另一篇博文~java位运算)就可以计算出整数部分P和余数部分S,并成功设置bit位为1,现在利用java来实现这个运算过程如下:

    //定义变量
    private int[] a; //数组存储元素的数组
    private int BIT_LENGTH = 32;//默认使用int类型
    private int P; //整数部分
    private int S; //余数
    private int MASK =  0x1F;// 2^5 - 1
    private int SHIFT = 5; // 2^n SHIFT=n=5 表示2^5=32 即bit位长度32

    计算代码

    /**
     * 置位操作,添加操作
     * @param i
     */
    public void set(int i){
         P = i >> SHIFT; //结果等同  P = i / BIT_LENGTH; 取整数 ①
         S = i & MASK;   //结果等同  S = i % BIT_LENGTH; 取余数 ②
    
         a[P] |= 1 << S;  //赋值设置该元素bit位为1               ③
         //将int型变量j的第k个比特位设置为1, 即j=j|(1<<k),上述3句合并为一句
         //a[i >> SHIFT ] |= (1 << (i & MASK));               ④
     }

    计算出P和S后,就可以进行赋值了,其中 a[P]代表数组中第P个元素,a[P] |= 1 << S 整句意思是把a[P]元素的第S+1位设置为1,注意从低位到高位设置,即从右到左,①②③合并为④,代码将更佳简洁。当然有添加操作,那么就会有删除操作,删除操作过程与添加类似,只不过删除是把相对应的bit位设置0,代表不存在该数值。

    /**
    * 置0操作,相当于清除元素
    * @param i
    */
    public void clear(int i){
       P =  i >> SHIFT; //计算位于数组中第?个元素 P = i / BIT_LENGTH;
       S =  i & MASK;   //计算余数  S = i % BIT_LENGTH;
       //把a[P]元素的第S+1个(从低位到高位)bit位设置为0
       a[P] &= ~(1 << S);
    
       //更优写法
       //将int型变量j的第k个比特位设置为0,即j= j&~(1<<k)
       //a[i>>SHIFT] &= ~(1<<(i &MASK));
    }

    与添加唯一不同的是,计算出余数S,利用1左移S位,再取反(~)操作,最后进行与(&)操作,即将a[P]元素的第S+1个(从低位到高位)bit位设置为0,表示删除该数字,这个计算过程大家可以自行推算一下。这就是位向量表示法的添加和清除方法,然后我们可以利用下述的get方法判断某个bit是否存在某个数字:

    /**
     * 读取操作,返回1代表该bit位有值,返回0代表该bit位没值
     * @param i
     * @return
     */
    public int get(int i){
        //a[i>>SHIFT] & (1<<(i&MASK));
        P = i >> SHIFT;
        S = i &  MASK;
        return Integer.bitCount(a[P] & (1 << S));
    }

    其中Integer.bitCount()是返回指定 int 值的二进制补码(计算机数字的二进制表示法都是使用补码表示的)表示形式的 1 位的数量。位向量运算整体代码实现如下:

    package com.zejian;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    
    /**
     * Created by zejian on 2017/5/13.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     * 位向量存储数据
     */
    public class BitVetory {
        private int count;
        private int[] a; //数组
        private int BIT_LENGTH = 32;//默认使用int类型
        private int P; //整数部分
        private int S; //余数
        private int MASK =  0x1F;// 2^5 - 1
        private int SHIFT = 5; // 2^n SHIFT=n=5 表示2^5=32 即bit位长度32
    
        /**
         * 初始化位向量
         * @param count
         */
        public BitVetory(int count) {
            this.count = count;
            a = new int[(count-1)/BIT_LENGTH + 1];
            init();
        }
    
        /**
         * 将数组中元素bit位设置为0
         */
        public void init(){
            for (int i = 0; i < count; i++) {
                clear(i);
            }
        }
    
        /**
         * 获取排序后的数组
         * @return
         */
        public List<Integer> getSortedArray(){
            List<Integer> sortedArray = new ArrayList<Integer>();
    
            for (int i = 0; i < count; i++) {
                if (get(i) == 1) {//判断i是否存在
                    sortedArray.add(i);
                }
            }
            return sortedArray;
        }
        /**
         * 置位操作,设置元素
         * @param i
         */
        public void set(int i){
            P = i >> SHIFT; //P = i / BIT_LENGTH; 取整数
            S = i & MASK; //S = i % BIT_LENGTH; 取余数
            a[P] |= 1 << S;
    
            //将int型变量j的第k个比特位设置为1, 即j=j|(1<<k),上述3句合并为一句
            //a[i >> SHIFT ] |= (1 << (i & MASK));
        }
    
        /**
         * 置0操作,相当于清除元素
         * @param i
         */
        public void clear(int i){
            P =  i >> SHIFT; //计算位于数组中第?个元素 P = i / BIT_LENGTH;
            S =  i & MASK;   //计算余数  S = i % BIT_LENGTH;
            a[P] &= ~(1 << S);
    
            //更优写法
            //将int型变量j的第k个比特位设置为0,即j= j&~(1<<k)
            //a[i>>SHIFT] &= ~(1<<(i &MASK));
        }
    
        /**
         * 读取操作,返回1代表该bit位有值,返回0代表该bit位没值
         * @param i
         * @return
         */
        public int get(int i){
            //a[i>>SHIFT] & (1<<(i&MASK));
            P = i >> SHIFT;
            S = i &  MASK;
            return Integer.bitCount(a[P] & (1 << S));
        }
    
        //测试
        public static void main(String[] args) {
            int count = 25;
            List<Integer> randoms = getRandomsList(count);
            System.out.println("排序前:");
    
            BitVetory bitVetory = new BitVetory(count);
            for (Integer e : randoms) {
                System.out.print(e+",");
                bitVetory.set(e);
            }
    
            List<Integer> sortedArray = bitVetory.getSortedArray();
            System.out.println();
            System.out.println("排序后:");
            for (Integer e : sortedArray) {
                System.out.print(e+",");
            }
    
            /**
             输出结果:
             排序前:
             6,3,20,10,18,15,19,16,13,4,21,22,24,2,14,5,12,7,23,8,1,17,9,11,
             排序后:
             1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,
             */
        }
    
        private static List<Integer> getRandomsList(int count) {
            Random random = new Random();
    
            List<Integer> randomsList = new ArrayList<Integer>();
            while(randomsList.size() < (count - 1)){
                int element = random.nextInt(count - 1) + 1;//element ∈  [1,count)
                if (!randomsList.contains(element)) {
                    randomsList.add(element);
                }
            }
            return randomsList;
        }
    }
    

    EnumSet原理

    有前面位向量的分析,对于了解EnumSet的实现原理就相对简单些了,EnumSet内部使用的位向量实现的,前面我们说过EnumSet是一个抽象类,事实上它存在两个子类,RegularEnumSet和JumboEnumSet。RegularEnumSet使用一个long类型的变量作为位向量,long类型的位长度是64,因此可以存储64个枚举实例的标志位,一般情况下是够用的了,而JumboEnumSet使用一个long类型的数组,当枚举个数超过64时,就会采用long数组的方式存储。先看看EnumSet内部的数据结构:

    public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
        implements Cloneable, java.io.Serializable
    {
        //表示枚举类型
        final Class<E> elementType;
        //存储该类型信息所表示的所有可能的枚举实例
        final Enum<?>[] universe;
        //..........
    }

    EnumSet中有两个变量,一个elementType用于表示枚举的类型信息,universe是数组类型,存储该类型信息所表示的所有可能的枚举实例,EnumSet是抽象类,因此具体的实现是由子类完成的,下面看看noneOf(Class<E> elementType)静态构建方法

      public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
            //根据EnumMap中的一样,获取所有可能的枚举实例
            Enum<?>[] universe = getUniverse(elementType);
            if (universe == null)
                throw new ClassCastException(elementType + " not an enum");
    
            if (universe.length <= 64)
                //枚举个数小于64,创建RegularEnumSet
                return new RegularEnumSet<>(elementType, universe);
            else
                //否则创建JumboEnumSet
                return new JumboEnumSet<>(elementType, universe);
        }

    从源码可以看出如果枚举值个数小于等于64,则静态工厂方法中创建的就是RegularEnumSet,否则大于64的话就创建JumboEnumSet。无论是RegularEnumSet还是JumboEnumSet,其构造函数内部都间接调用了EnumSet的构造函数,因此最终的elementType和universe都传递给了父类EnumSet的内部变量。如下:

    //RegularEnumSet构造
    RegularEnumSet(Class<E>elementType, Enum<?>[] universe) {
          super(elementType, universe);
      }
    
    //JumboEnumSet构造
    JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
          super(elementType, universe);
          elements = new long[(universe.length + 63) >>> 6];
      }

    在RegularEnumSet类和JumboEnumSet类中都存在一个elements变量,用于记录位向量的操作,

    //RegularEnumSet
    class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
        private static final long serialVersionUID = 3411599620347842686L;
        //通过long类型的elements记录位向量的操作
        private long elements = 0L;
        //.......
    }
    
    //对于JumboEnumSet则是:
    class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
        private static final long serialVersionUID = 334349849919042784L;
        //通过long数组类型的elements记录位向量
        private long elements[];
         //表示集合大小
        private int size = 0;
    
        //.............
        }

    在RegularEnumSet中elements是一个long类型的变量,共有64个bit位,因此可以记录64个枚举常量,当枚举常量的数量超过64个时,将使用JumboEnumSet,elements在该类中是一个long型的数组,每个数组元素都可以存储64个枚举常量,这个过程其实与前面位向量的分析是同样的道理,只不过前面使用的是32位的int类型,这里使用的是64位的long类型罢了。接着我们看看EnumSet是如何添加数据的,RegularEnumSet中的add实现如下

    public boolean add(E e) {
        //检测是否为枚举类型
        typeCheck(e);
        //记录旧elements
        long oldElements = elements;
        //执行位向量操作,是不是很熟悉?
        //数组版:a[i >> SHIFT ] |= (1 << (i & MASK))
        elements |= (1L << ((Enum)e).ordinal());
        return elements != oldElements;
    }

    关于elements |= (1L << ((Enum)e).ordinal());这句跟我们前面分析位向量操作是相同的原理,只不过前面分析的是数组类型实现,这里用的long类型单一变量实现,((Enum)e).ordinal()通过该语句获取要添加的枚举实例的序号,然后通过1左移再与 long类型的elements进行或操作,就可以把对应位置上的bit设置为1了,也就代表该枚举实例存在。图示演示过程如下,注意universe数组在EnumSet创建时就初始化并填充了所有可能的枚举实例,而elements值的第n个bit位1时代表枚举存在,而获取的则是从universe数组中的第n个元素值。

    这就是枚举实例的添加过程和获取原理。而对于JumboEnumSet的add实现则是如下:

    public boolean add(E e) {
        typeCheck(e);
        //计算ordinal值
        int eOrdinal = e.ordinal();
        int eWordNum = eOrdinal >>> 6;
    
        long oldElements = elements[eWordNum];
        //与前面分析的位向量相同:a[i >> SHIFT ] |= (1 << (i & MASK))
        elements[eWordNum] |= (1L << eOrdinal);
        boolean result = (elements[eWordNum] != oldElements);
        if (result)
            size++;
        return result;
    }

    关于JumboEnumSet的add实现与RegularEnumSet区别是一个是long数组类型,一个long变量,运算原理相同,数组的位向量运算与前面分析的是相同的,这里不再分析。接着看看如何删除元素

    //RegularEnumSet类实现
    public boolean remove(Object e) {
        if (e == null)
            return false;
        Class eClass = e.getClass();
        if (eClass != elementType && eClass.getSuperclass() != elementType)
            return false;
    
        long oldElements = elements;
        //将int型变量j的第k个比特位设置为0,即j= j&~(1<<k)
        //数组类型:a[i>>SHIFT] &= ~(1<<(i &MASK));
    
        elements &= ~(1L << ((Enum)e).ordinal());//long遍历类型操作
        return elements != oldElements;
    }
    
    
    //JumboEnumSet类的remove实现
    public boolean remove(Object e) {
            if (e == null)
                return false;
            Class<?> eClass = e.getClass();
            if (eClass != elementType && eClass.getSuperclass() != elementType)
                return false;
            int eOrdinal = ((Enum<?>)e).ordinal();
            int eWordNum = eOrdinal >>> 6;
    
            long oldElements = elements[eWordNum];
            //与a[i>>SHIFT] &= ~(1<<(i &MASK));相同
            elements[eWordNum] &= ~(1L << eOrdinal);
            boolean result = (elements[eWordNum] != oldElements);
            if (result)
                size--;
            return result;
        }

    删除remove的实现,跟位向量的清空操作是同样的实现原理,如下:

    至于JumboEnumSet的实现原理也是类似的,这里不再重复。下面为了简洁起见,我们以RegularEnumSet类的实现作为源码分析,毕竟JumboEnumSet的内部实现原理可以说跟前面分析过的位向量几乎一样。o~,看看如何判断是否包含某个元素

    public boolean contains(Object e) {
        if (e == null)
            return false;
        Class eClass = e.getClass();
        if (eClass != elementType && eClass.getSuperclass() != elementType)
            return false;
        //先左移再按&操作
        return (elements & (1L << ((Enum)e).ordinal())) != 0;
    }
    
    public boolean containsAll(Collection<?> c) {
        if (!(c instanceof RegularEnumSet))
            return super.containsAll(c);
    
        RegularEnumSet<?> es = (RegularEnumSet<?>)c;
        if (es.elementType != elementType)
            return es.isEmpty();
        //~elements取反相当于elements补集,再与es.elements进行&操作,如果为0,
        //就说明elements补集与es.elements没有交集,也就是es.elements是elements的子集
        return (es.elements & ~elements) == 0;
    }
    

    对于contains(Object e) 方法,先左移再按位与操作,不为0,则表示包含该元素,跟位向量的get操作实现原理类似,这个比较简单。对于containsAll(Collection<?> c)则可能比较难懂,这里分析一下,elements变量(long类型)标记EnumSet集合中已存在元素的bit位,如果bit位为1则说明存在枚举实例,为0则不存在,现在执行~elements 操作后 则说明~elements是elements的补集,那么只要传递进来的es.elements与补集~elements 执行&操作为0,那么就可以证明es.elements与补集~elements 没有交集的可能,也就是说es.elements只能是elements的子集,这样也就可以判断出当前EnumSet集合中包含传递进来的集合c了,借着下图协助理解:

    图中,elements代表A,es.elements代表S,~elements就是求A的补集,(es.elements & ~elements) == 0就是在验证A’∩B是不是空集,即S是否为A的子集。接着看retainAll方法,求两个集合交集

    public boolean retainAll(Collection<?> c) {
            if (!(c instanceof RegularEnumSet))
                return super.retainAll(c);
    
            RegularEnumSet<?> es = (RegularEnumSet<?>)c;
            if (es.elementType != elementType) {
                boolean changed = (elements != 0);
                elements = 0;
                return changed;
            }
    
            long oldElements = elements;
            //执行与操作,求交集,比较简单
            elements &= es.elements;
            return elements != oldElements;
        }

    最后来看看迭代器是如何取值的

     public Iterator<E> iterator() {
            return new EnumSetIterator<>();
        }
    
        private class EnumSetIterator<E extends Enum<E>> implements Iterator<E> {
            //记录elements
            long unseen;
    
            //记录最后一个返回值
            long lastReturned = 0;
    
            EnumSetIterator() {
                unseen = elements;
            }
    
            public boolean hasNext() {
                return unseen != 0;
            }
    
            @SuppressWarnings("unchecked")
            public E next() {
                if (unseen == 0)
                    throw new NoSuchElementException();
                //取值过程,先与本身负执行&操作得出的就是二进制低位开始的第一个1的数值大小
                lastReturned = unseen & -unseen;
                //取值后减去已取得lastReturned
                unseen -= lastReturned;
                //返回在指定 long 值的二进制补码表示形式中最低位(最右边)的 1 位之后的零位的数量
                return (E) universe[Long.numberOfTrailingZeros(lastReturned)];
            }
    
            public void remove() {
                if (lastReturned == 0)
                    throw new IllegalStateException();
                elements &= ~lastReturned;
                lastReturned = 0;
            }
        }

    比较晦涩的应该是

    //取值过程,先与本身负执行&操作得出的就是二进制低位开始的第一个1的数值大小
    lastReturned = unseen & -unseen; 
    //取值后减去已取得lastReturned
    unseen -= lastReturned;
    return (E) universe[Long.numberOfTrailingZeros(lastReturned)];

    我们通过原理图来协助理解,现在假设集合中已保存所有可能的枚举实例变量,我们需要把它们遍历展示出来,下面的第一个枚举元素的获取过程,显然通过unseen & -unseen;操作,我们可以获取到二进制低位开始的第一个1的数值,该计算的结果是要么全部都是0,要么就只有一个1,然后赋值给lastReturned,通过Long.numberOfTrailingZeros(lastReturned)获取到该bit为1在64位的long类型中的位置,即从低位算起的第几个bit,如图,该bit的位置恰好是低位的第1个bit位置,也就指明了universe数组的第一个元素就是要获取的枚举变量。执行unseen -= lastReturned;后继续进行第2个元素的遍历,依次类推遍历出所有值,这就是EnumSet的取值过程,真正存储枚举变量的是universe数组,而通过long类型变量的bit位的0或1表示存储该枚举变量在universe数组的那个位置,这样做的好处是任何操作都是执行long类型变量的bit位操作,这样执行效率将特别高,毕竟是二进制直接执行,只有最终获取值时才会操作到数组universe。

    ok~,到这关于EnumSet的实现原理主要部分我们就分析完了,其内部使用位向量,存储结构很简洁,节省空间,大部分操作都是按位运算,直接操作二进制数据,因此效率极高。当然通过前面的分析,我们也掌握位向量的运算原理。好~,关于java枚举,我们暂时聊到这。


    参考资料 《Thinking in Java》 And 《Effective Java》



    展开全文
  • Java枚举enum原理详解为什么要使用枚举类枚举类原理三、enum类所继承的父类java.lang.Enum其他用法附java.lang.Enum源码 为什么要使用枚举类 枚举类enum作为Java5新增特性的一部分,是用来代替常量的。 比如以下...
  • Java 枚举(enum) 详解7种常见的用法

    万次阅读 多人点赞 2016-08-11 11:14:45
    JDK1.5引入了新的类型——枚举。在Java中它虽然算个“小”功能,却给我的开发带来了“大”方便。...现在好了,有了枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。 Java代码 publ...
  • Java 枚举(enum) 详解五种常见的用法

    万次阅读 多人点赞 2018-07-13 10:45:55
    Java 枚举(enum) 详解五种常见的用法 一、枚举型常量 package com.yang; //首先枚举是一个特殊的class //这个class相当于final static修饰,不能被继承 //他的构造方法强制被私有化,下面有一个默认的构造方法...
  • 枚举类型Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。...
  • JDK1.5之前我们需要自定义枚举类,JDK1.5新增enum关键字。 1. 如何自定义枚举类呢?并不难,需要注意一下几个点: 1)枚举对象的属性不允许别改动,所以应该使用private final关键字。 2)枚举类中使用private ...
  • Java SE5提供了一种新的类型-Java枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。,需要的朋友可以参考下
  • java枚举类型enum

    2017-03-21 13:59:16
    为什么需要枚举? 一些方法在运行时,它需要的数据不能是任意的,而必须是一定范围内的值,...枚举APIJava中声明的枚举类,均是java.lang.Enum类的孩子,它继承了Enum类的所有方法。常用方法: • name() • ordinal
  • JAVA枚举赋值

    2018-09-20 10:30:00
    /*媒体类型*/ public enum MediaType { html(0), video(1), audio(2);private final int value; MediaType(int value) { this.value = value; } public int getValue() {...
  • JDK1.5引入了新的类型——枚举。在 Java 中它虽然算个...现在好了,有了枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。  Java代码  public enum Color {   RED, GREEN...
  • Java 中的枚举,是和类(class),接口(interface)平级 的概念,在用intellij idea 创建class文件的时候,可以看见: ...换句话说,我们可以把枚举Enum, 理解成...我们创建了一个名为TestEnum的枚举类型。那枚举
  • ava enum 枚举的spring boot2.x完美实现demo源码。java枚举类型,可以理解为一种特殊的java
  • 在项目实际开发过程中,经常会遇到对某些固定的值、字典项的定义的需求,很多项目经常... 枚举类默认会继承java.lang.Enum,由于java是单继承,所以在定义枚举类时不允许再继承其他类,但可以实现多个接口 枚举的比较.
  •  从前面的讲解中可以知道,使用enum关键字可以定义一个枚举,实际上此关键字表示的是java.lang.Enum类型,即使用enum声明的枚举类型就相当于定义了一个类!!而此类默认继承了java.lang.Enum类。 //java.lang....
  • 深入理解(2)Java枚举类型(enum)

    万次阅读 2019-05-01 10:29:55
    枚举类型Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。...
  • 用法:周一到周五 给 enum 自定义属性和方法 给 enum 对象加一下 value 的属性和 getValue() 的方法: ... * 枚举测试类 * * @author <a href="mailto:hemingwang0902@126.com">何明旺</a&g...
  • Java枚举[enum]Java枚举(enum)七种常见的用法汇总如下:用法一:常量用法二:switch用法三:向枚举中添加新方法用法四:覆盖枚举的方法用法五:实现接口用法六:使用接口组织枚举用法七:关于枚举集合的使用 Java枚举...
  • 枚举类的赋值和基本操作

    万次阅读 2017-08-05 09:04:58
    关键字enum——指明其后的标识符是一个枚举类型的名字。枚举常量表——由枚举常量构成。“枚举常量”或称“枚举成员”,是以标识符形式表示的整型量,表示枚举类型的取值。枚举常量表列出枚举类型的所有取
  • Java 枚举类型的基本想法非常简单: 这些类通过共有的静态final域为每个枚举常量导出一个实例。枚举类型没有可以访问的构造器,所以它是真的final类。客户端不能创建枚举类型的实例,也不能对它进行扩展,因此不存...
  • Java 枚举类和自定义枚举类和enum声明及实现接口1.枚举类1.枚举类的属性2.自定义枚举类2.Enum枚举类3.实现接口的枚举类 1.枚举类 注: JDK1.5之前需要自定义枚举类 JDK 1.5 新增的 enum 关键字用于定义枚举类 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 25,993
精华内容 10,397
关键字:

java枚举类型enum赋值

java 订阅