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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

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