Enum(枚举)

一.什么情况下使用枚举类?

  有的时候一个类的对象是有限且固定的,这种情况下我们使用枚举类就比较方便。

二.为什么不用静态常量来代替枚举类呢?

    public static final int SEASON_SPRING = 1;
    public static final int SEASON_SUMMER = 2;
    public static final int SEASON_FALL = 3;
    public static final int SEASON_WINTER = 4;

枚举类更加直观,类型安全。使用常量会有以下几个缺陷:

  1. 类型不安全。若一个方法中要求传入季节这个参数,用常量的话,形参就是int类型,开发者传入任意类型的int类型值就行,但是如果是枚举类型的话,就只能传入枚举类中包含的对象。

  2. 没有命名空间。开发者要在命名的时候以SEASON_开头,这样另外一个开发者再看这段代码的时候,才知道这四个常量分别代表季节。

三.枚举类入门

先看一个简单的枚举类

public enum SeasonEnum {
    SPRING,SUMMER,FALL,WINTER;
}

   1.enum和class、interface的地位一样

   2.使用enum定义的枚举类默认继承了java.lang.Enum,而不是继承Object类。枚举类可以实现一个或多个接口。

   3.枚举类的所有实例必须放在第一行展示,不需使用new 关键字,不需显式调用构造器。默认public static final修饰

   4.使用enum定义、非抽象的枚举类默认使用final修饰,不可以被继承。

   5.枚举类的构造器只能是私有的。

四.枚举类介绍

public enum SeasonEnum {
    SPRING("春天"),
    SUMMER("夏天"),
    FALL("秋天"),
    WINTER("冬天");
    
    private final String name;
    
    private SeasonEnum(String name) {
        this.name = name;
    }
}

  枚举类内也可以定义属性和方法,可是是静态的和非静态的。  

  实际上在第一行写枚举类实例的时候,默认是调用了构造器的,所以此处需要传入参数,因为没有显式申明无参构造器,只能调用有参数的构造器。、

  构造器需定义成私有的,这样就不能在别处申明此类的对象了。枚举类通常应该设计成不可变类,它的Field不应该被改变,这样会更安全,而且代码更加简洁。所以我们将Field用private final修饰。

五:枚举实现接口

public enum Operation {

    PLUS{
        @Override
        public double eval(double x, double y) {
            return x + y;
        }   
    },

    MINUS{
        @Override
        public double eval(double x, double y) {
            return x - y;
        }
    },

    TIMES{
        @Override
        public double eval(double x, double y) {
            return x * y;
        }
    },

    DIVIDE{
        @Override
        public double eval(double x, double y) {
            return x / y;
        } 
    };
    
    /**
     * 抽象方法,由不同的枚举值提供不同的实现。
     */
    public abstract double eval(double x, double y);
    
    public static void main(String[] args) {
        System.out.println(Operation.PLUS.eval(10, 2));
        System.out.println(Operation.MINUS.eval(10, 2));
        System.out.println(Operation.TIMES.eval(10, 2));
        System.out.println(Operation.DIVIDE.eval(10, 2));
    }
}

     枚举类可以实现一个或多个接口。与普通类一样,实现接口的时候需要实现接口中定义的所有方法,若没有完全实现,那这个枚举类就是抽象的,只是不需显式加上abstract修饰,系统化会默认加上。

  Operatio 类实际上是抽象的,不可以创建枚举值,所以此处在申明枚举值的时候,都实现了抽象方法,这其实是匿名内部类的实现,花括号部分是一个类体。

     我们可以看下编译以后的文件。生成了五个class文件,这样就证明了PLUS,MINUS,TIMES,DIVIDE是Operation的匿名内部类的实例。

六.switch语句里表达式可以是枚举值

     Java5新增了enum关键字,同时扩展了switch  case表达式中直接写入枚举值,不需加入枚举类作为限定。

public class SeasonTest {
    public static void main(String[] args) {
        SeasonEnum spring = SeasonEnum.SPRING;
        judge(spring);
    }

    public static void judge(SeasonEnum s) {
        switch(s) {
           case SPRING:
             System.out.println("春天适合踏青。");
             break;
           case SUMMER:
             System.out.println("夏天要去游泳啦。");
             break;
           case FALL:
             System.out.println("秋天一定要去旅游哦。");
             break;
           case WINTER:
             System.out.println("冬天要是下雪就好啦。");
             break;
        }
    }
}

为什么要引入枚举类?

个小案例

      你写了一个小程序,好久不用了,突然有一天,你想用一下它。程序要想正确运行,需要将今天星期几存到数据库里。这时候你开始犯难了。当初设计这个程序的时候,傻不拉几把这个字段设计为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; 
} 

机智如你,这个时候,只要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

   哟,不错。考虑的很详细。并且私有构造方法后,外界就不能创建该类的对象了,这样就避免了星期八星期九的出现,所有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;
    }

  当你需要一个整形数据的时候,只需要Weekday.toInt(Weekday.SUN);,看起来你好像完成了你的任务。但是,你有没有发现,这样写,好麻烦啊。如果想要扩展一下功能,大量的ifelse会让人眼花缭乱。有没有更好的方式呢?你大概已经知道了,没错,我们需要枚举类!我们先来看看一个简单的枚举类。

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

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

class Test2{
    public static void main(String[] args) {
        Weekday sun = Weekday.SUN;
        System.out.println(sun); // 输出 SUN
    }
}

看起来和上面的静态变量使用方式差不多,而且默认的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;
        //nowDay不为周六时,只进行了++操作 
        //nowDay为周六时 nextDayValue为0
        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 TestWeekDay{
    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)); 
        //nowday ====> SAT
        //nowday int ====> 6
        //nextday ====> SUN
    }
}  

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

枚举类的用法

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

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

如何使用它呢?先来看看它有哪些方法:

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

上图是枚举变量的方法。我们接下来会演示几个比较重要的:

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}
 
class Test3{
    public static void main(String[] args) {
 
        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.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的源码,会先判断是不是同一个枚举类的变量,然后再返回差值。

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

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

要点

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

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

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

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

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

枚举的高级使用方法:

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

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

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

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

public enum Weekday {
    MON,TUS,WED,THU,FRI,SAT,SUN
}

  我把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. 构造函数必须私有化。事实上,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;
    }
}

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

枚举类中的抽象类

  有抽象方法的类必然是抽象类,子类继承抽象类然后实现父类的抽象方法,但是呢,枚举类不能被继承,那抽象类怎么用呢?我们先来看代码:

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();
 
}

  你好像懂了点什么但又好像不太懂。为什么一个变量的后边可以带一个代码块并且实现抽象方法呢?别着急,带着这个疑问,我们来看一下枚举类的实现原理!

枚举类的实现原理

public enum Weekday {
    SUN,MON,TUS,WED,THU,FRI,SAT
}

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

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

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

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

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

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

RED(30) {
    @Override
    public TrafficLamp getNextLamp() {
        return GREEN;
    }
}

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

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

TrafficLamp RED = new TrafficLamp(30){
    @Override
    public TrafficLamp getNextLamp() {
        return GREEN;
    }
};

  而在枚举类中,我们只需要像上面那样写【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;
        }
    }
}

实现接口

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

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);
    }
}

使用接口组织枚举

public interface Food {
    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, LATTE, CAPPUCCINO
    }
 
    enum Dessert implements Food {
        FRUIT, CAKE, GELATO
    }
}

使用枚举创建单例模式

public enum EasySingleton{
    INSTANCE;
}

代码就这么简单,你可以使用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;
     }
}

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

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

public class Singleton{
    private static final Singleton INSTANCE = new Singleton();
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        return INSTANCE;
    }
}

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

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

最后总结一下:

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

  • switch()参数可以使用enum 

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

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

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

  本文到这里就差不多结束了。可能举得例子不是很恰当,代码写的不是很优雅,不过我只是用来引出枚举的,大家不要鸡蛋里头挑骨头哈哈。除此之外,还有两个枚举集合:【java.util.EnumSet和java.util.EnumMap】没有讲。关于枚举集合的使用会在后面讲集合框架的时候再详细讲解。

 

posted @ 2021-09-16 16:53  江南大才子  阅读(553)  评论(0编辑  收藏  举报