Java Enum

Enum

有时候变量的取值只有在一个有限的集合内。例如服装的尺码只有大、中、小和超大这四种尺寸。针对这种情况就可以自定义枚举类型。枚举类型包含有限个命名的值。

声明

public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }

这个声明定义的类型是一个类,它定义了四个实例,而且它们都是由public static final修饰的。

实例化一个枚举对象时,只能初始化为null或者集合中的某个常量值。

因此在比较两个枚举类型的值时,不需要调用equals,而直接使用"=="就可以(直接比较地址)。

上述声明中看到一个关键词enum这和classinterface类似。同样在一个Java源文件中,只允许有一个public修饰的枚举类,而且类名必须和文件名相同。

在声明中,定义了四个实例,他们都继承自java.lang.Enum,并实现了 java.lang.Seriablizable 和 java.lang.Comparable 两个接口。

进阶自定义Enum

如果有需要可以在Enum类中添加成员变量,构造器和方法。此时有以下几点需要注意:

  1. 在定义新的方法、构造器或者成员变量之前,必须先定义好Enum实例,同时给实例序列末尾加上";"。
  2. 添加成员变量时,必须注意在预先定义好的实例后添加括号,将成员变量初始化值放进去。它们会通过构造函数来初始化成员变量。
  3. 添加构造器时一定要用private修饰,私有化,这样一来在外部就不能实例化。构造器只有在构造枚举常量的时候被调用。
public enum Size {
    SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

    private Size(String abbreviation) {
        this.abbreviation = abbreviation;
    }

    private String abbreviation;

    public String getAbbreviation() {
        return abbreviation;
    }
}

继承自Enum方法

和直接通过public static final修饰声明常量不同的时,Enum常量为其提供了一系列方法。

toString()

这个实例方法在Enum中默认的实现是能够返回枚举常量名。

例如Size.SMALL.toString();将返回字符串"SMALL"。

当然可以在自定义枚举类时赋写它。

valueOf()

toString()的逆方法时静态方法valueOf()

例如Size s = Enum.valueOf(Size.class, "SMALL");将完成枚举类实例化。

这个其实类似Integer等基本类型包装类中的valueOf(),内部维护一个数组存储所有预定义的实例,通过valueOf()来返回堆内存中的地址。

values()

Enum类中有一个静态方法values(),它将返回一个包含全部枚举常量值的数组,顺序是按照声明的顺序。

例如Size[] values = Size.values();

oridinal()

该实例方法返回enum声明中枚举常量的位置,位置从0开始计数,顺序是按照声明的顺序。

例如Size.SMALL.oridinal();返回0。

name()

该实例方法返回enum声明时的常量名,和toString()类似。

compareTo()

实际上,Enum类有一个类型参数。实例化枚举类对象Size s = Size.SMALL;,实际上应该继承自Enum<Size>。该类型参数主要在compareTo(E other)方法中的other使用。

比较结果:

  1. 如果枚举常量出现在other之前,返回一个负值。
  2. 如果枚举常量==other,返回0。
  3. 否则返回正值。

也就是默认实现按照enum声明的常量顺序来比较。

Enum使用

常量

可以通过声明一个枚举类型,包含所需的常量值:

public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }

并且可以使用Enum的方法。

switch-case

jdk1.5之后,switch-case支持使用枚举类型进行判断。

在switch()括号中可以使用枚举类对象,case标签使用枚举类常量值。注意case标签中不需要指明枚举名,由switch()括号中确定。

enum Signal {
        GREEN, YELLOW
    }

    public class TrafficLight {
        Signal color = Signal.RED;

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

自定义枚举类

可以添加构造器,方法或者成员变量,满足上面讨论的几点要求,就可以实现。

代码示例在上面,进阶自定义Enum

覆盖Enum方法

一般默认的toString()方法无法满足平时开发的需求,那么就可以自己去实现逻辑。

        @Override
        public String toString() {
            return "the mobile's name is " + flagshipMachine +
                ". The mobile's build version is " + buildVersion;
        }

上述代码返回的字符串时关于手机信息。

实现接口

由于Java规定不能有多继承,而自定义的枚举类都继承自Enum,所以枚举类不能再继承其他类。那么有什么办法扩展枚举类中实例元素呢?

可以使用接口来进行扩展,实现多态特性。

原理

接口的作用:接口抽象了方法,由子类去实现它。在执行代码中通过调用实例变量的实际类型,来调用匹配方法名的方法。

这是一种编程思想,如果一开始,使用一个具体的类来实现描述一个手机,而此时描述的是塞班系统。日后科技的发展,有了Android、ios、WindowPhone...其他系统,那么只能放弃整个类,去重新实现一个。关键是放弃塞班类之后,需要在执行代码中去修改使用塞班类对象的地方。这个工作量非常大。此时可以使用接口思想来处理。

在一开始设计时,定义一个Phone接口,里面定义了所有关于手机的基本功能抽象方法,然后通过子类去实现它。在执行代码中通过Phone类型变量来调用相关手机基本功能方法。

参考

JAVA接口的作用

java 接口的作用和好处

枚举类实现接口

接口代码:

public interface Phone {
    void callPhone();
    void sendMessage();
}

枚举类代码:

    enum Ios implements Phone {
        APPLE(10, "Iphone");

        private int buildVersion;
        private String flagshipMachine;

        private Ios(int buildVersion, String flagshipMachine) {
            this.buildVersion = buildVersion;
            this.flagshipMachine = flagshipMachine;
        }

        public void saySiri() {
            System.out.println("Hello I'm siri!");
        }

        @Override
        public void callPhone() {
            System.out.println("Make a phone with FaceTime");
        }

        @Override
        public void sendMessage() {
            System.out.println("Send a message with IMessage");
        }

        @Override
        public String toString() {
            return "the mobile's name is " + flagshipMachine +
                ". The mobile's build version is " + buildVersion;
        }
    }

当然也可以在每一个预定义的实例后去实现接口抽象方法特定逻辑,但是在enum类中必须实现接口提供的抽象方法,因为enum类implements接口。

    enum BlackBerry implements Phone {
        BLACKBERRY {
            @Override
            public void callPhone() {
                System.out.println("Make phone with BBM");
            }

            @Override
            public void sendMessage() {
                System.out.println("send a message with BBM");
            }
        };
        @Override
        public void callPhone() {

        }

        @Override
        public void sendMessage() {

        }
    }

分组枚举类,返回某一类枚举

以模拟用户选择手机案例来实现。现在市场上主流的职能手机有三个:Android、iOS和WindowPhone。现在将环境抽象,只有这三种系统可以选择,而且每一种系统只有几个固定的手机制造商。

那么有四个枚举类,一是Android系统旗舰机枚举类;二是ios系统旗舰机枚举类;三是WindowPhone系统旗舰机枚举类;最后是供用户选择系统的枚举类。

接口分组枚举类

通过接口,可以将拥有同一共性的枚举类进行分类,同时还可以提供扩展。为什么采用接口进行分组,是因为接口内部的enum在编译时,默认会加上public static修饰,可以很方便的在接口外部通过接口名访问。

/**
* 用接口组织分组枚举类,同时实现接口共同方法,方便在代码中使用多态特性。
**/
public interface Phone {
    //每一个枚举类都有自己的方法
    enum Android implements Phone {
        GOOGLE(7, "Pixel"), SAMSUNG(6, "Galaxy"), XIAOMI(6, "XIAOMI"), ONEPLUS(6, "ONEPLUS_3");

        private int buildVersion;
        private String flagshipMachine;

        private Android(int buildVersion, String flagshipMachine) {
            this.buildVersion = buildVersion;
            this.flagshipMachine = flagshipMachine;
        }

        public void sayOkGoogle() {
            System.out.println("What can I help you ?");
        }

        @Override
        public void callPhone() {
            System.out.println("Make a phone with Google Hangouts");
        }

        @Override
        public void sendMessage() {
            System.out.println("Send a message with Google Hangouts");
        }
        @Override
        public String toString() {
            return "the mobile's name is " + flagshipMachine +
                ". The mobile's build version is " + buildVersion;
        }
    }

    enum Window implements Phone {
        NOKIA(10, "Lumia");

        private int buildVersion;
        private String flagshipMachine;

        private Window(int buildVersion, String flagshipMachine) {
            this.buildVersion = buildVersion;
            this.flagshipMachine = flagshipMachine;
        }

        public void sayCortana() {
            System.out.println("I'm Cortana,what can I Help you ?");
        }

        @Override
        public void callPhone() {
            System.out.println("Make a phone with Lumia");
        }

        @Override
        public void sendMessage() {
            System.out.println("Send a message with Lumia");
        }

        @Override
        public String toString() {
            return "the mobile's name is " + flagshipMachine +
                ". The mobile's build version is " + buildVersion;
        }
    }

    enum Ios implements Phone {
        APPLE(10, "Iphone");

        private int buildVersion;
        private String flagshipMachine;

        private Ios(int buildVersion, String flagshipMachine) {
            this.buildVersion = buildVersion;
            this.flagshipMachine = flagshipMachine;
        }

        public void saySiri() {
            System.out.println("Hello I'm siri!");
        }

        @Override
        public void callPhone() {
            System.out.println("Make a phone with FaceTime");
        }

        @Override
        public void sendMessage() {
            System.out.println("Send a message with IMessage");
        }

        @Override
        public String toString() {
            return "the mobile's name is " + flagshipMachine +
                ". The mobile's build version is " + buildVersion;
        }
    }

    void callPhone();
    void sendMessage();
}

定义了Phone接口,在里面定义了抽象的打电话,发短信功能的方法,由特定的系统枚举类实现。同时每一个枚举类都拥有各自独特的语音助手方法。最后覆写了toString()

枚举元素中的枚举
/**
* 扩展枚举类中的元素为枚举类。同时限定用户只能选择这三种常量。
**/
public enum PhoneUser {
    ANDROID(Phone.Android.class),
    WINDOW(Phone.Window.class),
    IOS(Phone.Ios.class);

    private Phone[] values;
    private PhoneUser(Class<? extends Phone> kind) {
        values = kind.getEnumConstants();
    }

    public Phone randomSelection() {
        return Enums.random(values);
    }
}

定义了一个用户选择枚举类,里面的每一个元素包含一个Phone接口的数组,存放对应系统的旗舰机枚举对象。

代码中有用到一个工具类,实现随机分配指定系统类型的一款手机。

public class Enums {
    private static Random random = new Random(47);

    public static Phone random(Phone[] values) {
        return values[random.nextInt(values.length)];
    }
}
测试
public class SelectPhone {

    public static void main (String[] args) {
        Phone mobile = null;
        Scanner in = new Scanner(System.in);
        System.out.println("What kind of mobile phone do you want?(Android,Windwo,Ios)");
        String input = in.next().toUpperCase();
        PhoneUser user = null;
        while(true) {
            try {
                user = Enum.valueOf(PhoneUser.class, input);
                break;
            }catch(IllegalArgumentException e) {
                System.out.println("Wrong system name,please input again!");
                input = in.next().toUpperCase();
            }
        }
        mobile = user.randomSelection();
        if(mobile instanceof Enum) {
            System.out.println("The mobile info is "  + ((Enum)mobile));
        }
        System.out.println("Do you want make a phone?(Y/N)");
        input = in.next().toUpperCase();
        if(input.equals("Y")) {
            mobile.callPhone();
        }

        System.out.println("Do you want send a message?(Y/N)");
        input = in.next().toUpperCase();
        if(input.equals("Y")) {
            mobile.sendMessage();
        }
        System.out.println("Do you want use voice assistant?(Y/N)");
        input = in.next().toUpperCase();
        if(input.equals("Y")) {
            if(mobile instanceof Phone.Android) {
                ((Phone.Android)mobile).sayOkGoogle();
            }else if(mobile instanceof Phone.Window) {
                ((Phone.Window)mobile).sayCortana();
            }else {
                ((Phone.Ios)mobile).saySiri();
            }
        }
    }
}

执行输出:

接口扩展枚举类.png

枚举类中的抽象方法

public enum Operation{
    PLUS {
        @Override
        public double calculate(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        @Override
        public double calculate(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        @Override
        public double calculate(double x, double y) {
            return x * y;
        }
    },
    DIVIDE {
        @Override
        public double calculate(double x, double y) {
            return x / y;
        }
    };
    //抽象方法,给每一个实例自己实现
    public abstract double calculate(double x, double y);
}

针对加减乘除操作枚举类,每一种算法都有自己实现的calculate()方法。注意,虽然enum类中有抽象方法,但是不能在enum关键词前加上abstract标注为抽象类(因为枚举类在编译时JVM会自动为它加上abstract)。

抽象枚举类.png

但是由于枚举类会根据预定义的实例来创建枚举常量值,所以在定义每个实例时都必须实现抽象方法。

问题

enum类中可以有静态变量吗?构造器中可以引用吗?

静态变量

enum中可以声明静态变量,或者静态常量。但是在Java规范中,规定了enum里的构造器、初始化器和初始化块中不得引用该enum中非编译时常量的静态成员域。这就涉及到了enum类加载过程。

enum类加载

将enum类的字节码文件反编译后可以看到:

javap enum.png
  1. Color被final关键词修饰,并且继承自Enum<Color>。
  2. 分别实现了两个静态方法,values()valuesOf()
  3. 构建了一个静态代码块,实例化预定义的类。

说明预定义的类是在enum类加载初始化阶段,调用构造器创建的,而且是最先执行的。

所以在enum类中构造器可以去引用编译期常量。

非编译期静态常量&静态变量

非编译期静态常量

例如

enum Color {  
    RED, GREEN, BLUE;  
    static final Map<String,Color> colorMap = new HashMap<String,Color>();  
    Color() {  
        colorMap.put(toString(), this);  
    }  
}  

代码中colorMap就是非编译期常量,而且它不会在类加载连接过程中初始化为null,只有在声明时初始化,或者静态代码块中初始化。所以没有Java规范(规定了enum里的构造器、初始化器和初始化块中不得引用该enum中非编译时常量的静态成员域),执行会出现尚未初始化报错。

有了Java规范,那么在编译时就无法通过,避免了运行时错误发生。但是可以通过方法来访问colorMap

enum Color {  
    RED, GREEN, BLUE;  
    static final Map<String,Color> colorMap = new HashMap<String,Color>();  
    Color() {  
        //colorMap.put(toString(), this);  
        registerValue();
    }
    private void registerValue() {  
        PowerOfTwo.map.put(value, this);  
    }  
}  

这样可以编译通过,但是一旦执行,就会抛出空指针异常,整个程序就会crash。

正确的写法是:

public enum Color{
    RED, GREEN, BLUE;
    public static final Map<String, Color> colorMap =
        new HashMap<>();
    static {
        for(Color c : Color.values()) {
            colorMap.put(c.toString(), c);
        }
    }
    //public static Map<String, Color> colorMap =
        //new HashMap<>();
}

在静态代码块中进行Map存储。

这里还有一点需要注意,类加载连接阶段会给静态变量初始化JVM默认值,而引用类型默认是null。而又有final修饰,并不会赋予默认值。如果像注释代码一样声明,那么编译会报非法向前引用的错误。所以必须在静态代码块前声明并且初始化。

静态变量

例如

public enum Operation{
    ...

    /**
    * 创建的实例个数,计数
    * 但是无法在构造器中直接引用,编译时会提示“初始化程序中对静态字段的引用不合法”
    **/
    public static int operationCount = 0;
    //抽象方法,给每一个实例自己实现
    public abstract double calculate(double x, double y);

    private Operation() {
        int i = operationCount; //编译时会提示“初始化程序中对静态字段的引用不合法”
        addInstantCount();
    }

    private void addInstantCount() {
        operationCount++;
    }
}

代码中基本类型静态变量operationCount,由于Java规范不可以直接在构造器中引用。但是可以通过方法addInstantCount()进行修改。

这是因为在类加载连接过程中,对基本类型变量会赋予JVM默认值,而且存储在方法区中,且地址唯一,没有副本。那么在实例化预定义类时操作的是同一块内存。所以在本例中operationCount用于计数实例个数,最终得到的值也是4。

但是引用类型无法以方法的形式在构造器中使用,因为类加载连接阶段初始化为null,运行时会空指针。

参考

Java中enum的静态成员的初始化

说说Java枚举类型

参考

Java enum的用法详解

Java编程思想笔记七

Java 中的枚举 (enum)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,505评论 18 399
  • 1、.java源文件: 一个以”.java“为后缀的源文件:只能有一个与文件名相同的类,可以包含其他类。 2、类方...
    Hughman阅读 1,429评论 1 9
  • 昨天在做besuper的小型交互游戏的时候,强默默走到我边上,然后看了一眼我的代码,又一如既往的开始喷我:你这写的...
    wuchaooooo阅读 640评论 2 0
  • 又是一年毕业季,以前总是看着哥哥姐姐们离去,殊不知------ It's my turn! 谈笑间,与同学们相处...
    卓_睿阅读 356评论 0 2
  • 关于 此项目是 vue + element-ui 构建的后台管理系统,是后台项目node-elm 的管理系统,所有...
    苍都阅读 728评论 0 2