一文读懂《Effective Java》第3条:用私有构造器或者枚举类型强化Singleton属性

点击上方蓝字关注我们

Singleton 指仅仅被实例化一的类,通常用于代表那些本质上唯一的系统组件,比如窗口管理器或者文件系统。

使类成为Singleton 会使它的客户端测试变得困难,因为无法给Singleton 替换模拟实现,除非它实现一个充当其类型的接口。

实现Singleton 共有3种方法,在Java 1.5发行版之前实现有两种;在Java 1.5发行版之后实现Singleton 还有第三种方法,下面进行依次介绍。

第一种:公有静态类是个final域

构造器保持为私有,导出公有静态成员,客户端可以访问该类的唯一实例。

public class Elvis {


  public static final Elvis INSTANCE = new Elvis();


  //构造器保持为私有,导出公有静态成员,客户端可以访问该类的唯一实例
  private Elvis(){
    //...
  }


  public void leaveTheBuilding(){
    //...
  }
}

私有构造器仅被调用依次,用来实例化公有静态final域 Elvis.INSTANCE。由于缺少共有的或者受保护的构造器,所以保证了Elvis 全局唯一性。

但要注意一点:享有的客户端可以借助 AccessiableObject.setAccessible方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,代码里面做个检测判断,在被要求创建第二个实例时抛出异常。

第二种:公有成员是个静态工厂方法

实现Singleton 的第二种方法,公有的成员是个静态工厂方法:

public class Elvis2 {
  // 私有成员
  private static final Elvis2 INSTANCE = new Elvis2();


  // 静态方法公有成员
  public static Elvis2 getInstance(){
    return INSTANCE;
  }


  //构造器保持为私有,导出公有静态成员,客户端可以访问该类的唯一实例
  private Elvis2(){
    //...
  }


  public void leaveTheBuilding(){
    //...
  }
}

对于静态方法Elvis2.getInstance() 的所有调用,都会返回一个对象引用,所以永远不会创建其他实例(但是反射机制依然可以绕开这个设计,因此最好构造器同样做个判断)。

公有域方法的好处是,组成类成员的声明,很清楚的表明这个类是一个Singleton。公有的静态域是final的, 所以该域总是包含相同的对象引用。

但公有域方法在性能上不再有任何优势了,因为现代的JVM 实现几乎都能够将静态工厂方法的调用内联化(Inline Method)

另外,工厂方法有2个优势:

  • 在不改变API的前提下,我们可以改变此类是否应该为Singleton 的想法。工厂方法返回该类的唯一实例,但会容易被修改,比如改为每个调用该方法的线程返回一个唯一的实例。

  • 第二个优势,与泛型有关。为了让Singleton 类变成可序列化的(Serializable),仅仅在对象声明中加上“implements Serializable” 是不够的。为了维护并保证Singleton,必须声明所有实例域都是瞬时的(transient),并提供一个readResolve方法。否则每次反序列化都会创建一个新实例。

  // readResolve method to preserve singleton property
  private Object readObject(){
    // Return the one true Elvis and let the garbage collector take care of the Elvis impersonator
    return INSTANCE;
  }

第三种:包含单个元素的枚举类型

第三种实现Singleton 的方法是编写一个包含单个元素的枚举类型:

public class SingletonCase6 {
  enum SingletonEnum {
    //创建一个枚举对象,该对象天生为单例
    INSTANCE;
    private SingletonCase6 singleton;


    //私有化枚举的构造函数
    private SingletonEnum() {
      System.out.println("-----1 init constructor-----");
      singleton = new SingletonCase6();
    }


    public SingletonCase6 getSingleton() {
      System.out.println("-----2 return singleton-----");
      return singleton;
    }
  }


  private SingletonCase6() {
  }


  public static SingletonCase6 getInstance(){
    System.out.println("-----3 getInstance -----");
    return SingletonEnum.INSTANCE.getSingleton();
  }


  public static void main(String[] args) {
    getInstance();
  }
}

输出结果:

-----3 getInstance -----
-----1 init constructor-----
-----2 return singleton-----

这种方法在功能上跟公有域方法相近,但它更加简洁,无偿的提供了序列化机制,绝对防止多次实例化,即使是面对复杂的序列化或者反射攻击的时候。

总结

上面的第三种方法元素的枚举类型已经成为实现Singleton 最佳方法

单例模式实际上有5种(懒汉模式/饿汉模式/双锁检测模式/内部类/枚举类),相关的单例模式参考文章:

  • 设计模式学习(五):单例模式 (上)

    https://blog.csdn.net/qq_29166327/article/details/82229348?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160351095319195264762715%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=160351095319195264762715&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_v1~rank_blog_v1-3-82229348.pc_v1_rank_blog_v1&utm_term=%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F&spm=1018.2118.3001.4187

  • 设计模式学习(五):单例模式 (下)

    https://blog.csdn.net/qq_29166327/article/details/103587276?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160351095319195264762715%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=160351095319195264762715&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_v1~rank_blog_v1-4-103587276.pc_v1_rank_blog_v1&utm_term=%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F&spm=1018.2118.3001.4187

—END—

扫描二维码

获取技术干货

后台技术汇

点个“在看”表示朕

已阅

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值