美文网首页设计模式
单例模式之枚举类enum

单例模式之枚举类enum

作者: 我要进大厂 | 来源:发表于2022-08-10 20:24 被阅读0次

    通过枚举实现单例模式

    枚举类实现单例模式的优点

    对于饿汉式单例模式和懒汉式单例模式了解的同学,使用以上两种单例模式,我们均可以通过反射的方式破坏单例模式的特性。那么,如何才能避免程序开发中使用反射的方式破坏单例的这种行为呢?接下来用过枚举的方式来创建的单例模式就可以避免上述的问题。

    直接上代码:

    public enum EnumSingleton {
        INSTANCE;
        //枚举类可以自定义一些属性
        private Object data;
    
        public Object getData() {
            return data;
        }
    
        public void setData(Object data) {
            this.data = data;
        }
    }
    

    获取EnumSingleton 单例对象

       public static void main(String[] args) {
           // instance为单例
            EnumSingleton instance = EnumSingleton.INSTANCE;  
    }
    

    尝试通过反射破坏单例

    我们尝试使用反射的方式创建实例,看是否能够创建成功:

    public class EnumSingletonTest {
        public static void main(String[] args) {
            EnumSingleton enumSingleton=EnumSingleton.getInstance();
            enumSingleton.setData(new Object());
            System.out.println("创建单例模式:"+enumSingleton);
            try{
                Class clazz= EnumSingleton.class;
                Constructor c=clazz.getDeclaredConstructor(String.class,int.class);
                c.setAccessible(true);
                Object object=c.newInstance();
                System.out.println(object);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    

    会出现以下报错:

    java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
        at com.example.singleton.EnumSingletonTest.main(EnumSingletonTest.java:16)
    

    我们直接定位到报错源码的位置(Constructor.java:417)发现如下内容:

        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);
                }
            }
            if ((clazz.getModifiers() & Modifier.ENUM) != 0)
                throw new IllegalArgumentException("Cannot reflectively create enum objects");
    

    也就是说,在通过反射调用方法newInstance()创建实例时,源码会对class的修饰符做一个判断(clazz.getModifiers() & Modifier.ENUM) != 0)。如果创建实例的类是用enum修饰的,那么不能通过反射创建实例(Cannot reflectively create enum objects)
    ————————————————

    版权声明:本文为CSDN博主「沐谦」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/jl19960421/article/details/115036490


    深入了解 enum

    首先给出文章的结论:

    • 枚举是类类型
    • 每个枚举常量都是所属枚举类的对象
    • 枚举类在加载完成后,无法再进行实例化操作
    • 枚举是线程安全的

    java 中的枚举在实现上非常简单,以下就是一个枚举的例子:

    public enum FruitEnum {
        APPLE
    }
    

    但是如果反编译 FruitEnum.class,会发现编译器在背后默默的做了大量的工作,以下是反编译结果:

    public final class FruitEnum extends Enum {
        public  static final FruitEnum APPLE;
        private static final FruitEnum $VALUES[];
    
        private FruitEnum(String s, int i) {
            super(s, i);
        }
      
        public static FruitEnum[] values() {
            return (FruitEnum[])$VALUES.clone();
        }
    
        public static FruitEnum valueOf(String name) {
            return (FruitEnum)Enum.valueOf(FruitEnum, name);
        }
      
        static {
            APPLE = new FruitEnum("APPLE", 0);
            $VALUES = (new FruitEnum[] {
                APPLE
            });
        }
    }
    

    下面根据反编译结果说明枚举的几个特性。

    • 枚举的实际类型
    public final class FruitEnum extends Enum
    

    说明枚举是类,并且是用 final 修饰的类,意味着枚举不能再被继承扩展。而我们声明枚举类时使用的 enum 只是一个关键字。

    其次,所有的枚举都继承了一个基类 Enum ,该基类为枚举提供了一些通用方法。

    • 枚举的构造函数

    在 FruitEnum 中,我们并没有定义构造函数,但在反编译的代码中,我们发现编译器自动帮我们添加了以下私有构造函数:

    private FruitEnum(String s, int i) {
        super(s, i);
    }
    

    该私有构造函数只是简单调用了父类 Enum 的构造函数:

    protected Enum(String name, int ordinal) {
        this.name = name;       // 枚举常量的名称
        this.ordinal = ordinal; // 枚举常量在枚举类的位置,从0开始
    }
    

    实际上,编译器只允许私有的枚举类构造函数,并且显示定义的枚举构造函数经编译器后,都会被添加 name 和 ordinal 2个参数,用以调用父类 Enum 的构造函数。

    • 枚举常量

    再看反编译后的 FruitEnum 中的成员变量:

    public  static final FruitEnum APPLE;
    private static final FruitEnum $VALUES[];
    

    其中 FruitEnum APPLE 对应我们定义的枚举常量 APPLE,而 FruitEnum $VALUES[] 则是容纳所有枚举常量的数组。

    这两个变量初始化操作如下:

    static {
        APPLE = new FruitEnum("APPLE", 0);
        $VALUES = (new FruitEnum[] { APPLE });
    }
    

    可知,每个枚举常量都是定义它的类的对象,各枚举对象都是用 public static final 修饰的,这也是枚举对象被称为枚举常量的原因。

    其次,FruitEnum APPLE 和 FruitEnum $VALUES[] 都是在静态代码块中进行初始化,因此在 JVM 记载完成枚举类后,各枚举常量都已被

    创建完毕。由于枚举类中只允许出现私有构造函数,固无法再实例化新的枚举对象。

    枚举类实例化

    无法通过构造函数这种一般手段实例化枚举类,那么通过反射呢?

    先尝试以下形式:

    Class<FruitEnum> fruitEnumClazz = FruitEnum.class;
    FruitEnum fruitDemo1 = fruitEnumClazz.newInstance();
    

    抛出了以下错误:

    因为 Class.newInstance 内部是通过目标Class的 无参构造函数 创建实例的,而上文已经说了 FruitEnum 的实构造函数是:

    private FruitEnum(String s, int i) {
        super(s, i);
    }
    

    固会找不到对应的构造函数,抛出异常。

    换种反射方式,先获取正确的构造函数:

    // 获取 FruitEnum 的构造函数
    Constructor<FruitEnum> fruitEnumConstructor = fruitEnumClazz.getDeclaredConstructor(String.class, int.class);
    // 获取访问权限
    fruitEnumConstructor.setAccessible(true);
    // 实例化
    FruitEnum fruitDemo2 = fruitEnumConstructor.newInstance();
    

    成功获取了 FruitEnum 的构造函数,然而还是抛出了异常:

    在 JVM 层面禁止了通过反射构造枚举实例的行为。

    然而除了反射外,还可以通过 Object.clone() 方法克隆一个已存在的对象。但是这种方式也是不行的,看 Enum 类中的 clone() 方法:

    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
    

    Enum 类重写了 Obejct 的 clone()方法,只要尝试通过 clone() 方法构造枚举对象,都会抛出异常,并且该方法被设置为不能再重写。

    • 单例

    从编译器、JVM,再到 java 内部设计,层层把关,封死了实例化枚举类的这一企图。因此有一种作法是通过枚举的形式实现单例,下面给个示例:

    public class EnumSingleton{
        private EnumSingleton() {
          
        }
      
        public static EnumSingleton getInstance(){
            return Singleton.INSTANCE.getInstance();
        }
        
        private static enum Singleton {
            INSTANCE;
            
            private EnumSingleton singleton;
            
            // JVM 保证了此方法绝对只调用一次
            private Singleton(){
                singleton = new EnumSingleton();
            }
            
            public EnumSingleton getInstance(){
                return singleton;
            }
        }
    }
    
    • 线程安全

    枚举常量都是 static 类型的,在枚举类加载完成后,会进行枚举常量的初始化,之后枚举类无法再实例化和修改。java 的类加载、初始化过程是线程安全的,因此创建一个 enum 是线程安全的。

    作者:EricAlpha
    链接:https://www.jianshu.com/p/ec811fc70b08
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

        本文标题:单例模式之枚举类enum

        本文链接:https://www.haomeiwen.com/subject/qiytgrtx.html