Java-枚举详解

枚举

简介

枚举是Java 5中新增的一部分内容,他是一种特殊的类,一般表示一组常量,它和普通类一样可以使用构造器(必须私有),定义成员变量和方法,也能实现一个或多个接口,但枚举类不能继承其他类

如何使用

1.语法
  • 声明枚举类必须使用 enum 来实现,简洁语法如下:
enum-modifiers enum enumname {
        enum-body,
}
复制代码
  • 说明:
    • enum-modifiers 访问修饰符
    • enumname 声明枚举类的名称
    • enum-body 表示枚举的成员
2.举例
  • 声明一个名叫Color的枚举类
// 1. 定义
public enum Color {
    BLACK,
    WHITE
}

// 2. 使用
class Test {
    public static void main(String[] args) {
        System.out.println(Color.BLACK);  // BLACK
    }
}

复制代码
3.底层原理
  • 下面我们通过 jad 工具来反编译Color类, 通过 jad -sjava Color.class 反编译出一份 java文件
// final修饰,无法被继承
public final class Color extends Enum {

    // 为了避免 返回的数组修改,而引起内部values值的改变,返回的是原数组的副本
    public static Color[] values() {
        return (Color[]) $VALUES.clone();
    }

    // 按名字获取枚举实例
    public static Color valueOf(String name) {
        return (Color) Enum.valueOf(em / Color, name);
    }

    // 私有的构造函数
    private Color(String name, int ordinal) {
        super(name, ordinal);
    }

    // enum第一行的声明的变量,都对应一个枚举实例对象
    public static final Color BLACK;
    public static final Color WHITE;
    //
    private static final Color $VALUES[];

    // 静态域初始化,说明在类加载的cinit阶段就会被实例化,jvm能够保证类加载过程的线程安全
    static {
        BLACK = new Color("BLACK", 0);
        WHITE = new Color("WHITE", 1);
        $VALUES = (new Color[]{
                BLACK, WHITE
        });
    }
}
复制代码
  • 从反编译的Color类中可以看出,在enum关键字的类中,第一行 (准确的说是第一个分号前) 定义的变量,都会生成一个 Color实例,且它是在静态域中进行初始化的, 而静态域在类加载阶段进行初始化,所以枚举对象是线程安全的,且由JVM来保证

ps: 这里我联想到八种单例模式的实现方式,其中一种就是使用enum来实现的,那时候很是不能理解,它是怎么创建出对象的?仅仅是定义了一个常量吗? 大致代码如下


public enum Singleton {
    INSTANCE;

    public void doWhatever() {
        // todo ...
    }
}

复制代码

现在实际上我们可以知道,其实这个INSTANCE其实反编译了之后是一个对象(public static final Singleton INSTANCE = new Singleton()),且由于是JVM控制的,所以有且只有一个对象实例,写法极其优雅

《Effective Java 》和《Java与模式》都非常推荐这种方式,使用这种方式方式实现枚举可以有什么好处呢?

《Effective Java》这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。 —-《Effective Java 中文版 第二版》

《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

4.枚举的唯一性

意思是枚举类型它拥有的实例在编写的时候,就已经确定下,不能通过其他手段进行创建,且枚举变量在jvm有且只有一个对应的实例 为了达到这种效果,这背后都做了些什么呢?

  1. 类加载时创建,保证线程安全

从Color类中可以看出,Color对象是在静态域创建,由类加载时初始化,JVM保证线程安全,这样就能确保Color对象不会因为并发同时请求而错误的创建多个实例.

  1. 对序列化进行特殊处理,防止反序列化时创建新的对象

我们知道一旦实现了Serializable接口之后,反序列化时每次调用readObject()方法返回的都是一个新创建出来的对象
而枚举则不同,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过Enum的valueOf()方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化进行定制,因此禁用了writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve等方法


   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);
   }
   /**
    * prevent default deserialization
    */
   private void readObject(ObjectInputStream in) throws IOException,
       ClassNotFoundException {
       throw new InvalidObjectException("can't deserialize enum");
   }

   private void readObjectNoData() throws ObjectStreamException {
       throw new InvalidObjectException("can't deserialize enum");
   }

复制代码
  1. 有构造函数, 无法正常的 new出对象

图片[1]-Java-枚举详解-一一网
就算我们没有使用 private 关键字进行修饰其默认访问修饰符依旧是 private

  1. 无法通过 clone()方法,克隆对象
 
   /**
    * Throws CloneNotSupportedException.  This guarantees that enums
    * are never cloned, which is necessary to preserve their "singleton"
    * status.
    */
   protected final Object clone() throws CloneNotSupportedException {
       throw new CloneNotSupportedException();
   }  
   
复制代码
  1. 无法通过反射的方式创建枚举对象

枚举类型,在 JVM 层面禁止了通过反射构造枚举实例的行为,如果尝试通过反射创建,将会报Cannot reflectively create enum objects

  static void reflectTest() throws Exception {
      // 获取类对象
      Class<?> cls = Class.forName("com.xiao.basetest.base.enums.Color");
      // 获取 color 的构造函数
      Constructor<?> constructor = cls.getDeclaredConstructor(String.class, int.class);
      // 获取访问权限
      constructor.setAccessible(true);
      // 实例化
      Object reflectColor = constructor.newInstance("name", 0);
  }

  // 报错
  Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
  at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
  at Main.reflect(Main.java:24)
  at Main.main(Main.java:13)
复制代码

常用方法

方法名称 描述
values() 以数组形式返回枚举类型的所有成员
valueOf() 将普通字符串转换为枚举实例
compareTo() 比较两个枚举成员在定义时的顺序
ordinal() 获取枚举成员的索引位置
  1. values() 循环输出成员变量,并使用 ordinal() 输出索引值
public static void main(String[] args) {
        for (Color value : Color.values()) {
            System.out.println("当前索引值为:"+value.ordinal()+",数据为:"+value);
        }
    }

// 结果: 
当前索引值为:0,数据为:BLACK
当前索引值为:1,数据为:WHITE

复制代码
  1. 通过 valueof() 获取枚举实例,并使用 compareTo() 比较定义顺序

public enum Color {
    BLACK,
    WHITE,
    YELLOW;
}

class Test {
    public static void main(String[] args) {
        Color color = Color.valueOf("WHITE");
        compare(color);
    }

    public static void compare(Color color){
        for (Color value : Color.values()) {
            System.out.println(color+"与"+value+"的比较结果是"+color.compareTo(value));
        }
    }
}

结果:  

WHITE与BLACK的比较结果是1
WHITE与WHITE的比较结果是0
WHITE与YELLOW的比较结果是-1

复制代码

枚举的成员变量与方法

  • 除了枚举常量外, enum是一个完整的类,它也可以定义成员变量编写自己的构造方法以及成员方法,甚至实现接口.

这里需要注意,枚举类不能继承其他类,因为在编译时它已经继承了Enum,java无法多继承


// 实现Runnable接口,在这个类中没有意义,只是为了举例
public enum Color implements Runnable {
    WHITE("黑色",1),
    BLACK("白色",2),
    YELLOW("黄色",3);

    private final String value; //定义成员变量
    private final Integer index;

    // 自定义构造,虽然没有写private,但是默认就是private
    Color(String value,Integer index) {
        this.value = value;
        this.index = index;
    }

    // 自定义方法
    public void draw() {
        System.out.println("绘制 " + value);
    }

    // 重写方法
    @Override
    public String toString() {
        return "hello I'm "+value+", my index is "+index;
    }

    // 实现接口方法
    @Override
    public void run() {
        // todo ...
    }
}

class Test {
    public static void main(String[] args) {
        for (Color value : Color.values()) {
            System.out.println(value);
        }
    }
}

结果:
  
hello I'm 黑色, my index is 1
hello I'm 白色, my index is 2
hello I'm 黄色, my index is 3

复制代码
  • 既然说到了方法,这里还要引申一下就是如果在枚举中使用了抽象方法,会发生什么呢?以及引出抽象方法后枚举策略模式相关说明

特定的常量类型与主体中的方法或行为有关时,即当数据与行为之间有关联时,可以考虑使用枚举来实现策略模式

假设我们在枚举类中定义一个抽象方法,那么它应该在哪个地方被实现呢,肯定是枚举实例变量中被实现,且必须实现


public enum Operation {
    PLUS{
        @Override
        public int apply(int a, int b) {
            return a+b;
        }
    },
    MINUS{
        @Override
        public int apply(int a, int b) {
            return a-b;
        }
    };

    public abstract int apply(int a,int b);

    public static void main(String[] args) {
        System.out.println(Operation.PLUS.apply(1, 2));
    }
}

复制代码

这种模式适用于当输入数据一样时,但需要做不同的操作时,可以采用这种方式来进行处理

复杂枚举

EnumSet,EnumMap并不常用,这里不做过多解释,想了解的可以参考 传送门

参考资料

csdn-Java枚举详解
菜鸟教程-枚举
简书-java枚举全面解读

本文由博客一文多发平台 OpenWrite 发布!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享