Android设计模式之单例模式(防止反序列化和反射构造器)

单例模式是像我们这些小白使用的设计模式之一,确保单例对象的类必须保证仅有一个实例,整个系统只有一个全局对象,避免产生过多的对象造成资源浪费。

  • 定义:确保一个类只有一个实例,而且能自行实例化并向整个系统提供这个实例。

  • 使用场景:比如访问数据库时候,或者app更新的Download实例。

  • 关键点:
    1,构造函数不对外开发,一般为Private(私有化使得不能通过new的形式构造单例类对象)
    2 ,通过一个静态方法或者枚举返回单例类对象
    3,确保单例类对象有且只有一个(确保多线程下也只有一个对象)
    4,确保单例类对象在反序列化不会重新构建对象

单例获取方式:

  • 饿汉单例模式(不管了,先new出来,太饿了)
public class SingletonTest {
    //自己内部定义一个静态对象,供自己使用
    private static final SingletonTest sSingletonTest = new SingletonTest();

    //构造函数私有
    private SingletonTest() {
    }

    //公有的静态方法,向外暴露获取单例对象的接口
    public static SingletonTest getInstance(){
        return sSingletonTest;
    }
}

  • 懒汉模式(需要的时候才去new对象,懒)

    使用synchronized修饰,获取单例是一个同步方法,会检查到其他线程有没有使用。保证在多线程下单例对象唯一。但是,每次获取单例模式都会进行同步,造成资源浪费,所以一般不建议使用。

public class SingletonTest {
    
    private static SingletonTest sSingletonTest;
    //构造函数私有
    private SingletonTest() {
    }
    public static synchronized SingletonTest getInstance(){
        if(sSingletonTest == null){
            sSingletonTest = new SingletonTest();
        }
        return sSingletonTest;
    }
}
  • DCL(double check lock)双层检锁模式

    DCL方式优点是:能在需要的时候初始化单例,又能保证线程安全,而且单例对象初始化后再调用getInstance不进行同步锁,避免资源浪费。

public class SingletonTest {

    private volatile static SingletonTest sSingletonTest = null;
    private  SingletonTest(){

    };

    public static  SingletonTest getInstance(){
      if(sSingletonTest == null){
          //同步代码块的部分,如果已经初始化了,就不会造成同步浪费
          synchronized (SingletonTest.class){
              if(sSingletonTest == null){
                  sSingletonTest = new SingletonTest();
              }
          }
      }
      return sSingletonTest;

    };
    
}
  • 静态内部类创建单例

静态内部类只有在被引用的时候才会初始化,静态变量 sSingletonTest 被创建出来。继承了懒加载的延时加载特性。而且静态变量又能保证唯一性,也是线程安全。

public class   SingletonTest {
    //静态内部类,
    private static class HelperSinger{
        private static SingletonTest sSingletonTest = new SingletonTest();
    }

    private SingletonTest(){}

    public static SingletonTest getInstace(){
        return HelperSinger.sSingletonTest;
    }
}
  • 枚举单例
    枚举类型:枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。

    推荐使用枚举,首先枚举跟java中其他类是一样的,不仅有字段还可以有方法,而且枚举还能保证枚举实例的创建是线程安全的。而且不会被反序列化反射构造器创建新的对象,推荐使用。

public enum  SingletonTest {
    INSTANCE;

    public  void  doYourNeed(){
        //你的逻辑
    }
}

防止反序列化
上面几种情况除了枚举,都是可以通过序列化将一个单例的实例对象写到磁盘,然后在读取回来,从而有效的获取一个实例。先看未处理参数的对象地址:
如果你的单例类不需要序列化,可以不管这个过程
单例类,实现Serializable 接口,才能序列化。

public class   SingletonTest implements Serializable {
    //静态内部类,
    private static class HelperSinger{
        private static SingletonTest sSingletonTest = new SingletonTest();
    }

    private SingletonTest(){}

    public static SingletonTest getInstace(){
        return HelperSinger.sSingletonTest;
    }
}

测试类,完成对象序列化存储和读取,比较对象地址。

public class SerializeTest {

    public static void main(String[] args) throws Exception{
        SingletonTest s1 = SingletonTest.getInstace();
        SingletonTest s2 = SingletonTest.getInstace();
        System.out.println("s1对象的地址="+s1); // sc1,sc2是同一个对象
        System.out.println("s2对象的地址="+s1);

        //通过序列化方法构造的对象(首先序列化对象,实现Serializable接口)
        //把s1对象写入本地
        FileOutputStream fos = new FileOutputStream("test.out");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s1);
        oos.close();
        fos.close();

        //将保存对象从文件中读出来赋值给s3
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.out"));
        //通过序列化得到的对象
        SingletonTest s3 = (SingletonTest)ois.readObject();
        ois.close();
        System.out.println("s3对象的地址="+s3);

    }
}

运行结果如下:
运行结果

对比发现:s1和s2是通过单例模式getInstace()创建处理的,对象地址为同一个;
而将s1序列化后再读取出来的对象s3,对象地址是不一样的,所以不能保证单例,是两个实例

解决方案:
反序列化操作提供了一个很特别的钩子函数hook函数,类中具有一个私有的、被实例化得方法readResolve(),这个方法可以让我们控制对象的反序列化,所以可以添加该方法,返回当前的单例实例。
修改后如下:

public class   SingletonTest implements Serializable {
    //静态内部类,
    private static class HelperSinger{
        private static SingletonTest sSingletonTest = new SingletonTest();
    }

    private SingletonTest(){}

    public static SingletonTest getInstace(){
        return HelperSinger.sSingletonTest;
    }

    //防止反序列化产生多个对象
    private Object readResolve() throws ObjectStreamException{
        return SingletonTest.getInstace();
    }

}

再次运行SerializeTest ,打印台输出如下:
这里写图片描述
成功的防止了反序列化多个对象。

通过反射产生多个实例
反射可以获取到对象的实例应该是非常常见的一种,这里的单例模式一样会被反射构造器拿到实例。代码如下:
单例类(静态内部法):

public class  SingletonTest  {
    //静态内部类,
    private static class HelperSinger{
        private static SingletonTest sSingletonTest = new SingletonTest();
    }
    //私有构造函数
    private SingletonTest(){}

    public static SingletonTest getInstace(){
        return HelperSinger.sSingletonTest;
    }
}

测试类,通过反射获取实例

public class ReflectTest {

    public static void main(String[] args) throws Exception{
        SingletonTest s1 = SingletonTest.getInstace();
        SingletonTest s2 = SingletonTest.getInstace();
        System.out.println("s1对象的地址="+s1); // sc1,sc2是同一个对象
        System.out.println("s2对象的地址="+s1);

       //通过反射构造器
        //取得Class对象
        Class<SingletonTest> clazz = (Class<SingletonTest>) Class.forName("com.hu.test.test.SingletonTest");
        //获取到无参构造器
        Constructor<SingletonTest> constructor = clazz.getDeclaredConstructor(null);
        //跳过权限检查(私有无视掉)
        constructor.setAccessible(true);
        //获取实例
        SingletonTest s5 = constructor.newInstance();
        SingletonTest s6 = constructor.newInstance();
        //s5和s6不是同一个对象
        System.out.println("s5对象的地址="+s5);
        System.out.println("s6对象的地址="+s6);
    }
}

运行测试类ReflectTest ,看控制台输出对象地址:
这里写图片描述
s5和s6和s1分别对应的不同实例,通过反射获得多个对象。

解决办法,在构造器里面抛错。如果想创建第二个实例就报错。修改如下:

public class  SingletonTest  {
    //静态内部类,
    private static class HelperSinger{
        private static SingletonTest sSingletonTest = new SingletonTest();
    }
    //私有构造函数
    private SingletonTest(){
        // 防止反射获取多个对象的漏洞
        if (null != HelperSinger.sSingletonTest) {
            throw new RuntimeException("单例被反射");
        }
    }

    public static SingletonTest getInstace(){
        return HelperSinger.sSingletonTest;
    }
}

再次运行测试类ReflectTest ,看控制台输出对象地址:
这里写图片描述

把两者结合下,既能防止反序列化产生多个对象,又能不被反射构造器创建多个对象。而静态内部类又具有懒加载机制和线程安全,跟枚举单例一样是单例模式很好的选择。合并防止反序列化和反射代码如下:

public class  SingletonTest implements Serializable {
    //静态内部类,
    private static class HelperSinger{
        private static SingletonTest sSingletonTest = new SingletonTest();
    }
    //私有构造函数
    private SingletonTest(){
        // 防止反射获取多个对象的漏洞
        if (null != HelperSinger.sSingletonTest) {
            throw new RuntimeException("单例被反射");
        }
    }

    public static SingletonTest getInstace(){
        return HelperSinger.sSingletonTest;
    }

    //防止反序列化产生多个对象
    private Object readResolve() throws ObjectStreamException {
        return SingletonTest.getInstace();
    }
}

总上所述,为了防止反序列化和反射构造器等问题,可以使用枚举单例模式,也可以使用改造后静态内部类单例方式。当然还是枚举类比较简洁,几句代码。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值