枚举

2023/11/12 Java基础

# 1、枚举概念

# 1.1、枚举初识

JDK1.5引入了新的类型——枚举。枚举(enumerated)类型是一种特殊的类,用来定义固定的数值常量集合。要创建枚举,需要使用enum关键字。 枚举是一种更安全,更具可读性的数据类型,可以用来表示固定的值集合,如一周的天数、月份等。

看个简单案例:颜色枚举

public enum Color{
    RED,BLUE,GREEN,BLACK;
}
1
2
3

枚举类型是一种特殊的类,用来表示固定的值集合。

# 1.2、枚举类的使用规则和应用场景

  • 使用规则

  • 类的对象是确定的有限个数。
  • 当需要定义一组常量时,建议使用枚举。
  • 如果枚举类中只有一个对象,则可以作为单例模式的实现方法。
  • 枚举类不能被继承
  • 枚举类不能被单独的new创建对象
  • 枚举类中的枚举成员是用,隔开的,多个枚举成员之间用_隔开
  • 如果枚举类中的只有一个或多个枚举成员,其他什么都没有,我们在用,隔开的同时。最后可以省略;结束符。
  • 应用常见

这里列举一些比较常用的枚举使用场景:

  • 星期: Monday(星期一)、Tuesday(星期二)、Wednesday(星期三)、Thursday(星期四)、Firday(星期五)、Saturday(星期六)、Sunday(星期日)
  • 性别: Man(男)、Woman(女)
  • 季节: Spring(春天)、Summer(夏天)、Autumn(秋天)、Winter(冬天)
  • 支付方式: Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银行卡)、CreditCard(信用卡)
  • 订单状态: Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配货)、Delivered(已发货)、Return(退货)、Checked(已确认)
  • 线程状态: Establish(创建)、Ready(就绪)、Run(运行)、Obstruct(阻塞)、Die(死亡)

# 2、枚举类

# 2.1、枚举类定义

语法

访问修饰符 enum 枚举类型名称{
    枚举的成员1,枚举的成员2,....,枚举的成员n;
}
1
2
3

案例1

public enum Color{
    RED,BLUE,GREEN,BLACK;
}
1
2
3

解读

  • 在这个枚举类型中,有四个常量:RED, BLUE, GREEN和BLACK。这四个常量都是Color类型的对象,可以将它们用作变量的值或参数的类型。
  • 例如,可以使用Color.RED来表示红色,Color.BLUE来表示蓝色,以此类推。
  • 枚举类型可以用于程序设计中需要定义一组有限的可选值的场景,比如表示颜色、星期几等等。通过使用枚举类型,可以增强代码的可读性和可维护性。

案例2

public enum Colors {
    RED ("Red", 1), GREEN ("Green", 2), BLUE ("Blue", 3);

    private final String name;
    private final int index;
    
    Colors(String name, int index) {
        this.name = name;
        this.index = index;
    }
    
    public String getName() {
        return name;
    }
    
    public int getIndex() {
        return index;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

解读

  • 这段代码定义了一个名为Colors的枚举类型。这个枚举类型具有三个实例:RED,GREEN,BLUE。每个实例都有两个属性,一个String类型的name和一个int类型的index。
  • 每个枚举实例的构造方法是Colors(String name, int index)。当你创建一个枚举实例时,例如 RED ("Red", 1),其实就是在调用这个构造方法,将"Red"和1传入当做参数。
  • name和index是每个枚举实例的字段(成员变量),并且他们被声明为final,意味着一旦被初始化,它们的值无法被修改。这是因为枚举是被设计为不可变类(immutable class),所有的属性应该是不可变的。
  • 然后,这个枚举类型提供了两个公有的方法:getName()和getIndex(),分别用于在外部获取name和index的值。
  • 总的来说,这个枚举类型表达的是有颜色的概念,每个颜色有一个名字和一个索引值。例如,红色("Red")的索引值为1,绿色("Green")的索引值为2,蓝色("Blue")的索引值为3。
  • 通过这个枚举类,你可以在其他代码中使用Colors.RED、Colors.GREEN、Colors.BLUE来表示特定的颜色,以及调用getName()和getIndex()方法获取每种颜色对应的名字和索引值。

# 2.2、枚举使用

Java 中的每一个枚举都继承自 java.lang.Enum 类。当定义一个枚举类型时,每一个枚举类型成员都可以看作是 Enum 类的实例,这些枚举成员默认都被 final、public, static 修饰,当使用枚举类型成员时,直接使用枚举名称调用成员即可。

// 常量
System.out.println(Colors.RED);
// 获取枚举成员的名称
System.out.println(Colors.RED.getName());

/**
 * 输出结果:
 * RED
 * Red
 */
1
2
3
4
5
6
7
8
9
10

# 2.3、枚举类方法

所有枚举实例都可以调用 Enum 类的方法,常用方法如表所示:

返回值 方法 描述
String name() 获取枚举成员的名称
static T valueOf(Class<T> enumType, String name) 获取指定枚举成员名称和类型的枚举成员
String[] values() 获取枚举成员的所有值
int compareTo(E o) 比较此枚举与指定对象的顺序
int hashCode() 获取枚举成员的哈希值
int ordinal() 获取枚举成员的序数(第一个枚举成员位置为0)
String toString() 返回枚举成员名称
Class<E> getDeclaringClass() 获取枚举成员的类对象

# 2.4、自定义枚举类

自定义枚举的特点

  • 不需要提供setXxx方法,因为枚举对象值通常为只读
  • 对枚举对象、属性使用 final + static 共同修饰,实现底层优化
  • 枚举对象名通常全部大写,遵循常量命名的规范
  • 枚举对象根据需要,可以有多个属性

上面的案例都是简单的枚举使用,那我们挑选一个比较经典的春夏秋冬来实现自定义枚举类

/**
 * 自定义季节的枚举类
 */
public class Season {
    //声明Season对象的属性,为private final修饰
    private final String seasonName;

    //私有化构造器,并为对象赋值
    private Season(String seasonName) {
        this.seasonName = seasonName;
    }

    //提供当前枚举的多个对象,为public static final修饰
    public static final Season SPRING = new Season("春天");
    public static final Season SUMMER = new Season("夏天");
    public static final Season AUTUMN = new Season("秋天");
    public static final Season WINTER = new Season("冬天");

    //提供外界通过getter方法来获取枚举对象的属性
    public String getSeasonName() {
        return seasonName;
    }

    //重写toString方法,以便打印出枚举结果
    @Override
    public String toString() {
        return "Season{" +
                "seasonName='" + seasonName + '\'' +
                '}';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

测试使用

public class SeasonTest {
    public static void main(String[] args) {
        Season spring = Season.SPRING;
        System.out.println(spring);
    }
}
1
2
3
4
5
6

由此看来,我们就可以根据类名来句点出常量对象了!

# 3、枚举类集合

为了更好地支持枚举类型,java.util 中添加了两个新类:EnumMap 和 EnumSet。使用它们可以更高效地操作枚举类型。

# 3.1、EnumMap

  • EnumMap 是专门为枚举类型量身定做的 Map 实现。虽然使用其他的 Map(如 HashMap)实现也能完成枚举类型实例到值的映射,但是使用 EnumMap 会更加高效。
  • HashMap 只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以 EnumMap 使用数组来存放与枚举类型对应的值,使得 EnumMap 的效率非常高。

关于EnumMap集合的使用与HashMap是一致的,没有什么特殊的。至于EnumMap集合的方法,我这里列举一下。

返回值 方法 描述
void clear() 移除所有映射关系。
EnumMap clone() 返回EnumMap集合。
boolean containsKey(Object key) 包含此键,则返回true
boolean containsValue(Object value) 包含一个或多个键映射到的该指定值,则返回true
Set> entrySet() 返回映射键值关系的Set集合
boolean equals(Object o) 比较对象与映射的相等关系
V get(Object key) 获取指定键映射的值,如果没有,返回null
Set<K> keySet() 返回所有键的Set集合
V put(K key, V value) 将指定键值存储在EnumMap集合中
void putAll(Map m) 将所有键值对存储在集合中
V remove(Object key) 如果存在映射关系,则移除该映射关系
int size() 返回存在映射关系的数量
Collection<V> values() 返回此映射中所包含值的 Collection集合

EnumMap的使用

  • EnumMap集合是我们new出来的对象,创建出来的对象需要传入一个枚举的类对象,才返回一个Map集合。Map集合是键值对形式存储,所以我们在写EnumMap集合的泛型时,根据需求来写,如果需要键是某枚举类型,我们泛型就写它。如果有枚举类是值的要求,那就泛型中的值写枚举类。键值对都要求是枚举那也是OK的,我们写泛型时都写需求的枚举类即可。除了创建对象和存储对象需要指定枚举类外,其他的与HashMap基本相同。

如下,我在创建EnumMap集合时执行的Week枚举类的类对象,泛型的键写的是Week枚举类,值写的Integer,这就意味着我们在put(存储键值对)的时候,键需要存储Week枚举类中的枚举成员,值需要存储Integer数值。

EnumMap<Week, Integer> map = new EnumMap<>(Week.class);
map.put(Week.MONDAY, 1);
map.put(Week.THURSDAY, 4);
System.out.println(map);            //{MONDAY=1, THURSDAY=4}
1
2
3
4

# 3.2、EnumSet

  • EnumSet 是枚举类型的高性能 Set 实现,它要求放入它的枚举常量必须属于同一枚举类型

EnumSet 提供了许多工厂方法以便于初始化

返回值 方法 描述
static EnumSet<E> allOf(Class<E> elementType) 创建一个包含指定元素类型的所有元素的枚举 set。
EnumSet<E> clone()` 返回一个set集合。
static EnumSet<E> complementOf(EnumSet<E> s) 创建一个其元素类型与指定枚举set相同的set集合
(新集合中包含原集合所不包含的枚举成员)
static EnumSet<E> copyOf(EnumSet<E> s) 创建一个其元素类型与指定枚举 set 相同的枚举 set集合
(新集合中包含与原集合相同的枚举成员)
static EnumSet<E> copyOf(Collection<E> s) 创建一个从指定 collection 初始化的枚举 set
static EnumSet<E> noneOf(Class<E> elementType) 创建一个具有指定元素类型的空枚举 set
static EnumSet<E> range(E from, E to) 创建一个最初包含由两个指定端点所定义范围内
的所有元素的枚举 set。
static EnumSet<E> of 创建一个最初包含指定元素的枚举 set。
注意:可以指定多个元素,所以在这里我没有列举参数

使用方法跟Map差不多

//创建一个包含Week所有枚举元素的Set集合
EnumSet<Week> weeks = EnumSet.allOf(Week.class);
System.out.println(weeks);              // [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]

//打印Set集合中的元素
for (Week week1 : weeks) {
    System.out.print(week1 + " ");      // MONDAY TUESDAY WEDNESDAY THURSDAY FRIDAY SATURDAY SUNDAY
}
1
2
3
4
5
6
7
8

# 4、枚举的七种用法

# 4.1、常量

public enum Color {  
  RED, GREEN, BLANK, YELLOW  
} 
1
2
3

# 4.2、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;  
        }  
    }  
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4.3、向枚举中添加新方法

public enum Color {  
    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;  
    }  
    // 普通方法  
    public static String getName(int index) {  
        for (Color c : Color.values()) {  
            if (c.getIndex() == index) {  
                return c.name;  
            }  
        }  
        return null;  
    }  
    // get set 方法  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public int getIndex() {  
        return index;  
    }  
    public void setIndex(int index) {  
        this.index = index;  
    }  
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 4.4、覆盖枚举的方法

public enum Color {  
    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 toString() {  
        return this.index+"_"+this.name;  
    }  
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 4.5、实现接口

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);  
    }  
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 4.6、使用接口组织枚举

public interface Food {  
    enum CoffeeEnum implements Food{  
        BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO  
    }  
    enum DessertEnum implements Food{  
        FRUIT, CAKE, GELATO  
    }  
}  
/**
 * 测试继承接口的枚举的使用
 */
private static void test() {
    for (Food.DessertEnum dessertEnum : Food.DessertEnum.values()) {
        System.out.println(dessertEnum + "  ");
    }
    // 输出:FRUIT, CAKE, GELATO  

    for (CoffeeEnum coffee : CoffeeEnum.values()) {
        System.out.print(coffee + "  ");
    }
    // 输出:BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO  

    //搞个实现接口,来组织枚举,简单讲,就是分类吧。如果大量使用枚举的话,这么干,在写代码的时候,就很方便调用啦。
    //还有就是个“多态”的功能吧,
    Food food = Food.DessertEnum.CAKE;
    System.out.println(food);
    food = CoffeeEnum.BLACK_COFFEE;
    System.out.println(food);
    // 输出:BLACK_COFFEE,CAKE
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

想要理解过程可以看这篇:枚举类型——使用接口来组织枚举 (opens new window)

# 4.7、枚举集合的使用

使用EnumSet和EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的 key是enum类型,而value则可以是任意类型。

# 4.8、枚举与单例模式

利用枚举来实现单例模式,这种方式被认为是实现单例模式的最佳方法,它更简洁,自动支持序列化机制,绝对防止多次实例化。可以防止反序列化重新创建对象,以及防止反射攻击。

枚举单例结论

/**
 * 枚举单例
 */
public enum  SingletonEnum {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

单例模式到枚举单例演变过程

  • 单例模式可以说是最常使用的设计模式了,它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供这个实例。
  • 在实际应用中,线程池、缓存、日志对象、对话框对象常被设计成单例,总之,选择单例模式就是为了避免不一致状态,下面我们将会简单说明单例模式的几种主要编写方式,从而对比出使用枚举实现单例模式的优点。

首先看看饿汉式的单例模式:

/**
 * 饿汉式(基于classloder机制避免了多线程的同步问题)
 */
public class SingletonHungry {

    private static SingletonHungry instance = new SingletonHungry();

    private SingletonHungry() {
    }

    public static SingletonHungry getInstance() {
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

显然这种写法比较简单,但问题是无法做到延迟创建对象,事实上如果该单例类涉及资源较多,创建比较耗时间时,我们更希望它可以尽可能地延迟加载,从而减小初始化的负载,于是便有了如下的懒汉式单例:

/**
 * Created by wuzejian on 2017/5/9..
 * 懒汉式单例模式(适合多线程安全)
 */
public class SingletonLazy {

    private static volatile SingletonLazy instance;

    private SingletonLazy() {
    }

    public static synchronized SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这种写法能够在多线程中很好的工作避免同步问题,同时也具备lazy loading机制,遗憾的是,由于synchronized的存在,效率很低,在单线程的情景下,完全可以去掉synchronized,为了兼顾效率与性能问题,改进后代码如下:

public class Singleton {
    private static volatile Singleton singleton = null;

    private Singleton(){}

    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 这种编写方式被称为“双重检查锁”,主要在getSingleton()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。
  • 但是必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。
  • volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。
  • volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。

或许我们可以利用静态内部类来实现更安全的机制,静态内部类单例模式如下:

/**
 * Created by wuzejian on 2017/5/9.
 * 静态内部类
 */
public class SingletonInner {
    private static class Holder {
        private static SingletonInner singleton = new SingletonInner();
    }

    private SingletonInner(){}

    public static SingletonInner getSingleton(){
        return Holder.singleton;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

正如上述代码所展示的,我们把Singleton实例放到一个静态内部类中,这样可以避免了静态实例在Singleton类的加载阶段就创建对象,毕竟静态变量初始化是在SingletonInner类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。

  • 从上述4种单例模式的写法中,似乎也解决了效率与懒加载的问题,但是它们都有两个共同的缺点: 序列化可能会破坏单例模式,比较每次反序列化一个序列化的对象实例时都会创建一个新的实例,解决方案如下:
//测试例子(四种写解决方式雷同)
public class Singleton implements java.io.Serializable {     
   public static Singleton INSTANCE = new Singleton();     

   protected Singleton() {     
   }  

   //反序列时直接返回当前INSTANCE
   private Object readResolve() {     
            return INSTANCE;     
      }    
}  
1
2
3
4
5
6
7
8
9
10
11
12

使用反射强行调用私有构造器,解决方式可以修改构造器,让它在创建第二个实例的时候抛异常,如下:

public static Singleton INSTANCE = new Singleton();     
private static volatile  boolean  flag = true;
private Singleton(){
    if(flag){
    flag = false;   
    }else{
        throw new RuntimeException("The instance  already exists !");
    }
}
1
2
3
4
5
6
7
8
9

如上所述,问题确实也得到了解决,但问题是我们为此付出了不少努力,即添加了不少代码,还应该注意到如果单例类维持了其他对象的状态时还需要使他们成为transient的对象,这种就更复杂了,那有没有更简单更高效的呢?当然是有的,那就是枚举单例了,先来看看如何实现:

/**
 * 枚举单例
 */
public enum  SingletonEnum {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 代码相当简洁,我们也可以像常规类一样编写enum类,为其添加变量和方法,访问方式也更简单,使用SingletonEnum.INSTANCE进行访问,这样也就避免调用getInstance方法,更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射的问题。
  • 枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。
  • 同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性,这里我们不妨再次看看Enum类的valueOf方法:
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);
}
1
2
3
4
5
6
7
8
9
10

实际上通过调用enumType(Class对象的引用)的enumConstantDirectory方法获取到的是一个Map集合,在该集合中存放了以枚举name为key和以枚举实例变量为value的Key&Value数据,因此通过name的值就可以获取到枚举实例,看看enumConstantDirectory方法源码:

Map<String, T> enumConstantDirectory() {
    if (enumConstantDirectory == null) {
        //getEnumConstantsShared最终通过反射调用枚举类的values方法
        T[] universe = getEnumConstantsShared();
        if (universe == null)
            throw new IllegalArgumentException(
                getName() + " is not an enum type");
        Map<String, T> m = new HashMap<>(2 * universe.length);
        //map存放了当前enum类的所有枚举实例变量,以name为key值
        for (T constant : universe)
            m.put(((Enum<?>)constant).name(), constant);
        enumConstantDirectory = m;
    }
    return enumConstantDirectory;
}
private volatile transient Map<String, T> enumConstantDirectory = null;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

到这里我们也就可以看出枚举序列化确实不会重新创建新实例,jvm保证了每个枚举实例变量的唯一性。再来看看反射到底能不能创建枚举,下面试图通过反射获取构造器并创建枚举

public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
  //获取枚举类的构造函数(前面的源码已分析过)
   Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
   constructor.setAccessible(true);
   //创建枚举
   SingletonEnum singleton=constructor.newInstance("otherInstance",9);
}
1
2
3
4
5
6
7

执行报错

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at zejian.SingletonEnum.main(SingletonEnum.java:38)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
1
2
3
4
5
6
7
8

显然告诉我们不能使用反射创建枚举类,这是为什么呢?不妨看看newInstance方法源码:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    //这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 源码很了然,确实无法使用反射创建枚举实例,也就是说明了创建枚举实例只有编译器能够做到而已。
  • 显然枚举单例模式确实是很不错的选择,因此我们推荐使用它。
  • 但是这总不是万能的,对于android平台这个可能未必是最好的选择,在android开发中,内存优化是个大块头,而使用枚举时占用的内存常常是静态变量的两倍还多,因此android官方在内存优化方面给出的建议是尽量避免在android中使用enum。
  • 但是不管如何,关于单例,我们总是应该记住:线程安全,延迟加载,序列化与反序列化安全,反射安全是很重重要的。

# 5、深入理解枚举类

深入理解可以看看一些源码解析

参考:深入理解Java枚举 (opens new window)深入理解Java枚举类型(enum) (opens new window)