《Effective Java》学习笔记 - (3)用私有构造器或者枚举类型强化Singleton属性


前言

其实这里就是单例模式的介绍了,在这个要点中,对于一个单例对象,构造器通常被设置成私有的,同时还要防止其他的特殊方法去获取另外的实例。


用私有构造器或者枚举类型强化Singleton属性

1. 介绍

Singleton是指仅仅被实例化一次的类,通常被用来代表一个无状态的对象,如函数,或者那些本质上唯一的系统组件。值得注意的是:如果一个类成为Singleton,那么这个类的客户端测试就会有点麻烦,因为不可能给Singleton替换模拟测试,除非实现一个充当其类型的接口。



2. 实现方法 - 1

实现代码如下:

public class Elvis {

    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }

}

私有构造器仅仅在类的内部被用来调用一次创建唯一的 Elvis 实例对象,所以保证了 Elvis 的全局唯一性。



3. 实现方法 - 2

public class Elvis_1 {

    private static final Elvis_1 INSTANCE = new Elvis_1();

    private Elvis_1() {
    }
    
    public static Elvis_1 getInstance(){
        return INSTANCE;
    }

}

这种和上面的区别就是使用静态方法 getInstance 来代替了直接引用对象,这种方法的好处就是使用了一个静态方法能让调用者明白这就是一个单例对象。

对比公有域的方法,静态工厂方法的好处就是:

  1. 在不改变API的前提下,我们可以改变该类是否应该为Singleton。比如在 getInstance 方法中我们可以返回 new Elvis_1,这样就可以改变其单例的属性了,比较灵活切换
  2. 如果程序需要,可以编写一个泛型Singleton工厂
  3. 可以通过方法引用作为提供者,比如 Elvis::instance 就是一个 Supplier<Elvis>

以上三种条件,除非满足任意一种,否则优先考虑公有域(public - field) 的方法



4. 反射破坏和预防

当然了,尽管是使用了这种方法,仍然有可能会被反射破坏,下面就来演示一下:

public class TestElvis {
    public static void main(String[] args) throws Exception {
        final Constructor<Elvis> declaredConstructor = Elvis.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        //反射获取到的
        final Elvis elvis = declaredConstructor.newInstance();
        //提前定义好的单例
        final Elvis instance = Elvis.INSTANCE;
        System.out.println(elvis);      //com.jianglianghao.three.Elvis@1b0375b3
        System.out.println(instance);   //com.jianglianghao.three.Elvis@2f7c7260
    }
}

当然了,如果要防止这种情况,我们只需要使用一个count变量即可:

public class Elvis {

    private static volatile int count = 0;
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
        count++;
        if(count > 1){
            throw new RuntimeException("反射破坏单例失败");
        }
    }

}

这种情况下,还是上面的代码,运行结果就变成了:
在这里插入图片描述

同样的用法对于静态工厂来说也是有用的



5. 序列化破坏单例和预防

如果将该实例序列化之后再反序列化,那么Java的底层就会为我们创建一个新的对象,而不是原来的旧对象

public class TestElvis {
    public static void main(String[] args) throws Exception {
        final Elvis before = Elvis.INSTANCE;

        //我们把对象输出到一个文件上
        FileOutputStream fileOutputStream = new FileOutputStream("SingleTon.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(before);

        //再读取出来
        FileInputStream fileInputStream = new FileInputStream("SingleTon.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Object instance2 = objectInputStream.readObject();
        Elvis after = (Elvis)instance2;

        System.out.println(before); //com.jianglianghao.three.Elvis@52af6cff
        System.out.println(after);  //com.jianglianghao.three.Elvis@61a485d2
    }
}

源码就是调用了一个newInstance方法来创建新的,解决方法就是在 Elvis 类中加入一个 readResolve 方法,在这个方法里面返回Singleton对象,在源码中,调用了一个 invokeReadResolve 方法,这个方法会走到 Elvis 这个类中的 readResolve 方法中。

public class Elvis implements Serializable {

    private static volatile int count = 0;
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
        count++;
        if(count > 1){
            throw new RuntimeException("反射破坏单例失败");
        }
    }
    
    private Object readResolve(){
        return INSTANCE;
    }

}

经过这样设置之后结果如下,可以看到尽管是序列化和反序列化得到的结果都是一样的:
在这里插入图片描述



6. 枚举类

实现Singleton的第三种方法就是使用枚举类,并且枚举类是不会被反射破坏的,并且也是不会受序列化和反序列化的影响的,但是如果Singleton必须扩展成一个超类,而不是扩展Enum的时候,则不宜使用这个方法。






如有错误,欢迎指出!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值