Java日记(day13)--Java 枚举(enum)

189 阅读7分钟

Java 枚举(enum)

1、枚举类概述

Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等。像这种类的对象只有有限个,确定的,强烈建议使用枚举类,我们之前在多线程编程中提到的线程状态state就是一个枚举类

public enum State {
    NEW, //初始状态、开始状态
    RUNNABLE, //就绪状态、可运行状态。
    BLOCKED, //阻塞状态
    WAITING, //等待状态
    TIMED_WAITING, //超时等待状态
    TERMINATED; //终止状态、结束状态
}

JDK1.5之前需要自定义枚举类,JDK 1.5 及之后Java 枚举类使用 enum 关键字来定义,各个常量使用逗号,来分割。例如定义一个颜色的枚举类。

enum Color { 
    RED, GREEN, BLUE; 
}

枚举类对象的调用

enum Color{
    RED, GREEN, BLUE;
}
 
public class Test{
    public static void main(String[] args){
        Color c1 = Color.RED;//枚举类对象的调用
        System.out.println(c1);//RED
    }
}

3、自定义枚举类

步骤

  • 提供类的属性,声明为private final
  • 声明为final的属性,在私有构造器中初始化
  • 提供公共方法可以获取枚举类的属性
  • 创建共有的枚举类的多个对象
public class Season {

    //提供类的属性,声明为常量 private final
    private final String seasonName;
    private final String seasonDesc;

    //声明为final的属性,私有化构造器中初始化
    private Season(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    

    //创建当前枚举类的多个对象
    //public static final的,static可以方便类调用。由于不使用get方法。所以用public便于访问
    public static final Season SPRING = new Season("春天", "春暖花开");
    public static final Season SUNNER = new Season("夏天", "夏日炎炎");
    public static final Season ATAUM = new Season("秋天", "秋高气爽");
    public static final Season WINTER = new Season("冬天", "白雪皑皑");


    @Override
    public String toString() {
        return "Season [seasonName=" + seasonName + ", seasonDesc=" + seasonDesc + "]";
    }
    
    //提供公共方法获取枚举类的属性
    public String getSeasonName() {
        return seasonName;
    }
    //提供公共方法获取枚举类的属性 
    public String getSeasonDesc() {
        return seasonDesc;
    }
}

测试

//使用:调用枚举类对象
public static void main(String[] args) {
    Season spring = Season.SPRING; //类名.属性方式调用
    System.out.println(spring);//Season [seasonName=春天, seasonDesc=春暖花开]
}

4、使用Enum关键字定义枚举类

  • 枚举类对象的属性不应允许被改动,所以应该使用private final修饰
  • 枚举类的使用private final修饰的属性应该在构造器中为其赋值
  • 若枚举类显式的定义了带参数的构造器,则在列出枚举值时也必须对应的传入参数
  • 必须在枚举类的第一行声明枚举类对象
enum Season1 {
    
    //1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
    //会自动调用有参构造器
    SPRING("春天", "春暖花开"),
    SUMMER("夏天", "夏日炎炎"),
    AUTUMN("秋天", "秋高气爽"),
    WINTER("冬天", "冰天雪地");
 
    //2.声明Season对象的属性:private final 修饰
    private final String seasonName;
    private final String seasonDesc;
 
    //3.私有化类的构造器,并给对象属性赋值
    private Season1(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
 
    //4.其他诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }
 
    public String getSeasonDesc() {
        return seasonDesc;
    }
 
}

测试

不建议在Enum枚举类里面重写tostring方法,因为我们使用Enum关键字声明的枚举类继承于Enum类,而Enum类中重写了继承自Object类的toString()方法,Enum类中重写的toString()方法输出的就是枚举类对象所对应的常量名 ,那么我们打印枚举类对象,打印是什么呢,是地址值嘛?答:对象名

//使用:调用枚举类对象
public static void main(String[] args) {
    
    SeasonEnum spring = SeasonEnum.SPRING; //调用枚举类对象    
    System.out.println(spring);//SPRING
    
    //toString:返回枚举类型对象的名称
    System.out.println(spring.toString());//SPRING    
    System.out.println(SeasonEnum.class.getSuperclass());//父类Enum,class java.lang.Enum
}

5、Enum枚举类成员

枚举跟普通类一样可以用自己的变量、方法和构造函数,构造函数只能使用 private 访问修饰符,所以外部无法调用。枚举既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它。

public enum Color {
    RED, GREEN, BLUE; //三个对象就会自动调用私有化构造函数三次

    // 私有化构造函数
    private Color() {
        System.out.println("Constructor called for : " + this.toString());
    }

    public void colorInfo() {
        System.out.println("Universal Color");
    }


    public static void main(String[] args) {
        Color c1 = Color.RED;
        System.out.println(c1);
        c1.colorInfo();
    }
}

执行以上代码输出结果为:

Constructor called for : RED
Constructor called for : GREEN
Constructor called for : BLUE
RED
Universal Color

6、Enum类的常用方法

enum 定义的枚举类默认继承了 java.lang.Enum 类,并实现了 java.lang.Seriablizablejava.lang.Comparable 两个接口。values(), ordinal() 和 valueOf() 方法位于 java.lang.Enum 类中:

image.png

可以看到,枚举也提供了序列化机制。某些情况,比如我们要通过网络传输一个数据库连接的句柄,会提供很多帮助。

image.png

6.1、values()

  • 通过枚举类直接调用此方法返回枚举类的对象数组,也就是将枚举类的对象以一个一个数组元素合并成一个数组返回
  • 这个方法是一个静态方法
  • 该方法可以很方便地遍历所有枚举值
SeasonEnum [] values = SeasonEnum.values();
for(int i = 0; i < values.length ; i++){
    System.out.println(values[i]);
}

在学习多线程中我们曾经学到Thread类中有一个内部类State,State类就是通过enum关键字定义的枚举类,在State类中定义了线程运行的几种状态

Thread.State [] values = Thread.State.values();
for(int i = 0; i < values.length ; i++){
    System.out.println(values[i])
}

6.2、valueof(String objName)

  • 使用枚举类调用此方法,可以把一个字符串转换为对应的同名的枚举类对象
  • 这个方法是一个静态方法
  • 要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException非法的参数异常
/*
通过SeasonEnum类调用valueof()方法创建了一个WINTER对象并赋给了SeasonEnum类型的引用winter
*/
SeasonEnum winter = SeasonEnum.valueof("WINTER");
//这里输出的自然就是winter引用对应的枚举类对象的对象名 : WINTER
System.out.println(winter);

6.3、ordinal()方法

  • ordinal()方法可以找到每个枚举常量的索引,就像数组索引一样。
enum Color{
    RED, GREEN, BLUE;
}
 
public class Test{
    public static void main(String[] args){
        // 调用 values()
        Color[] arr = Color.values();
 
        // 迭代枚举
        for (Color col : arr){
            // 查看索引
            System.out.println(col + " at index " + col.ordinal());
        }
 
        // 使用 valueOf() 返回枚举常量,不存在的会报错 IllegalArgumentException
        System.out.println(Color.valueOf("RED"));
        // System.out.println(Color.valueOf("WHITE"));
    }
}

执行以上代码输出结果为:

RED at index 0
GREEN at index 1
BLUE at index 2
RED

7、实现接口的枚举类

  • 和普通 Java 类一样,枚举类可以实现一个或多个接口
  • 若每个枚举对象在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。
  • 若需要每个枚举对象在调用实现的接口方法呈现出不同的行为方式, 则可以让每个枚举对象分别来实现该方法

(1)实视接口,在enum类中实现抽象方法

interface Info{
    void show();
}
 
enum Season1 implements Info {
    
    //1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
    SPRING("春天", "春暖花开"),
    SUMMER("夏天", "夏日炎炎"),
    AUTUMN("秋天", "秋高气爽"),
    WINTER("冬天", "冰天雪地");
 
    //2.声明Season对象的属性:private final 修饰
    private final String seasonName;
    private final String seasonDesc;
 
    //3.私有化类的构造器,并给对象属性赋值
    private Season1(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
 
    //4.其他诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }
 
    public String getSeasonDesc() {
        return seasonDesc;
    }
 
    @Override
    public void show() {
        System.out.println("这是一个季节");
    }
 
}

(2)让枚举类的对象分别实现接口中的抽象方法

interface Info {
    void show();
}
 
enum Season1 implements Info {
    //1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
    SPRING("春天", "春暖花开") {
        @Override
        public void show() {
            System.out.println("这是一个季节");
        }
    },
    SUMMER("夏天", "夏日炎炎") {
        @Override
        public void show() {
            System.out.println("这是一个季节");
        }
    },
    AUTUMN("秋天", "秋高气爽") {
        @Override
        public void show() {
            System.out.println("这是一个季节");
        }
    },
    WINTER("冬天", "冰天雪地") {
        @Override
        public void show() {
            System.out.println("这是一个季节");
        }
    };
 
    //2.声明Season对象的属性:private final 修饰
    private final String seasonName;
    private final String seasonDesc;
 
    //3.私有化类的构造器,并给对象属性赋值
    private Season1(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
 
    //4.其他诉求1:获取枚举类对象的属性
    public String getSeasonName() {
        return seasonName;
    }
 
    public String getSeasonDesc() {
        return seasonDesc;
    }
 
}

测试:

public static void main(String[] args) {
    Season1.SPRING.show();//枚举类.对象.方法
}

结果

这是一个季节

8、内部类中使用枚举

枚举类也可以声明在内部类中:

public class Test{
    enum Color{
        RED, GREEN, BLUE;
    }
 
    // 执行输出结果
    public static void main(String[] args){
        Color c1 = Color.RED;
        System.out.println(c1); //RED
    }
}

9、在 switch 中使用枚举类

JDK 1.5 中可以在 switch 表达式中使用Enum定义的枚举类的对象作为表达式,case 子句可以直接使用枚举值的名字, 无需添加枚举类作为限定。 枚举类常应用于 switch 语句中:

enum Color{
    RED, GREEN, BLUE;
}
public class MyClass {
    public static void main(String[] args) {

        Color myVar = Color.BLUE;

        switch(myVar) {
            case RED:
                System.out.println("红色");
                break;
            case GREEN:
                System.out.println("绿色");
                break;
            case BLUE:
                System.out.println("蓝色");
                break;
        }
    }
}

执行以上代码输出结果为:

蓝色

由于 switch 是要在有限的可能值集合中进行选择,因此它和 enum 是绝佳的组合。一般来说,在 switch 中使用整数值,而枚举实例天生就是具备整数值的次序,并且可以通过 ordinal() 方法取得其次序(现在显然是编译器帮我们做了类似的工作),因此我们可以再 switch 语句中使用 enum。

10、利用枚举实现策略模式

public enum CalculationEnum {

    /**
     * 加法运算
     */
    ADD{
        @Override
        public double exec(double num1, double num2) {
            return num1 + num2;
        }
    },
    /**
     * 减法运算
     */
    SUB{
        @Override
        public double exec(double num1, double num2) {
            return num1 - num2;
        }
    };

    public abstract double exec(double num1, double num2);

    public static void main(String[] args) {

        double addNum = CalculationEnum.ADD.exec(1, 2.2);
        System.out.println(addNum);
        double subNum = CalculationEnum.SUB.exec(3.2, 1.1);
        System.out.println(subNum);

    }
}

11、利用枚举实现单例模式

传统的方式:

饿汉式

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

饿汉式的缺点就是,可能在还不需要此实例的时候就已经把实例创建出来了,没起到lazy loading的效果。优点就是实现简单,而且安全可靠

懒加载模式:lazy loaded

public class SingletonSafe {

    private static volatile SingletonSafe singleton;

    private SingletonSafe() {
    }

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

这就是用double checked locking 双重校验 方法实现的单例,这里的getInstance()方法要检查两次,确保是否实例INSTANCE是否为null或者已经实例化了,通过加锁 ,对懒汉式单例模式做了线程安全处理,可以保证同时只有一个线程走到第二个判空代码中去,这样保证了只创建 一个实例。这里还用到了volatile关键字来修饰singleton,其最关键的作用是防止指令重排。

枚举实现:

注意: 若枚举只有一个对象,则可以作为一种单例模式的实现方式。

上面这种模式,即使有DCL(double checked locking) 也可能会创建不止一个实例,尽管在Java5这个问题修复了(jdk1.5在内存模型上做了大量的改善,提供了volatile关键字来修饰变量),但是仍然对新手来说还是比较棘手

选择枚举作为Singleton的实现方式,相对于其他方式尤其是类似的饿汉模式主要有以下优点:

  • 代码简单
  • 自由序列化

默认枚举实例的创建是线程安全的,但是在枚举中的其他任何实例方法由程序员自己负责。

public enum Singleton {
    INSTANCE; //创建实例的时候会调用构造方法
    public void doSomething() {
        System.out.println("doSomething");
    }

}

public class Main {
    public static void main(String[] args) {
        Singleton.INSTANCE.doSomething(); //通过Singleton.INSTANCE来调用实例对象
    }
}

传统单例模式通过调用getInstance()方法 获得对象,而枚举类直接通过Singleton.INSTANCE来获得实例对象。从创建一个单例模式来看,枚举模式可以全部在一行内完成,并且枚举创建的单例在JVM层面上也能保证实例是线程安全的。

单元素的枚举类型已经成为实现Singleton的最佳方法。

单例是如何被保证的

首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举对象实例时会自动执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。 也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。

12、枚举是如何保证线程安全的

从下面代码出发

public enum Color {
    RED, GREEN, BLUE,yellow;
}

我们使用反编译后代码内容如下

image.png

通过反编译后代码我们可以看到类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承,同时我们看到这个类中有几个static类型的属性和方法。 因为static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的