前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >再议单例模式和静态类

再议单例模式和静态类

作者头像
四火
发布2022-07-15 20:14:40
3640
发布2022-07-15 20:14:40
举报
文章被收录于专栏:四火的唠叨四火的唠叨

单例模式还是静态类,这是一个老话题了,从我刚开始接触 Java 的时候就看到这样的讨论。在这里我总结一下,也添加一点点新东西。

首先要澄清和区别一些概念,“静态类” 和 “所有方法皆为静态方法的类”。

严格说来,Java 中的静态类,指的是 “static class” 这样修饰的类定义,语法上的要求,使得这样的类一定是内部类,换言之,“静态内部类” 是对它的完整定义。静态内部类最大的好处在于可以隐藏自己(只让自己被所在外层的类用到),同时又可以访问到所在外层类的属性。和 “非静态” 的内部类相比,它可以放置一些静态的成员变量和方法定义,而非静态类不可以;而且,静态内部类不能访问外层类非静态的属性。

但是,通常我们所说的 “静态类”,也是下文所述的 “静态类”,是指所有的方法、属性都是静态的类,同时,我们在使用它们的时候,直接调用它们的静态方法、访问其中的静态属性,而不需要对其实例化。这类所谓的 “静态类” 往往具备这样两个特点,一个是使用 final 修饰,它们往往没有子类;其二是构造器都被私有化了,不允许被构造实例。

1、单例模式便于 mock,可测性好。虽说静态方法也可以 mock(比如需要使用一些特殊的注解),但是毕竟相对还是麻烦一些,也没有那么灵活。

2、有人说单例模式可以做到 lazy load,但是静态类不行。这肯定是扯淡,静态类也完全可以做到第一次使用的时候再加载。不过,其中值得一提的是单例中的 Double Check Lock,这里做一个简单介绍。看下面的代码:

代码语言:javascript
复制
public Singleton {  
    private static Singleton instance;  
    private Number n = new Number();  
    public Number get() {  
        return this.n; //(1)  
    }  
    public static Singleton getInstance() {  
        if(instance == null) {  
            synchronized(Singleton.class) {  
                if(instance == null)  
                    instance = new Singleton();  
            } //(2)  
        }  
        return instance;  
    }  
} 

这是很常见的一种写法,不过,由于编译器的优化,允许出现主存和线程工作内存数据不一致问题,这就是 “DCL 失效” 的问题。上面的代码是其典型表现:

根据 JMM 规范,主存数据和工作内存的数据是允许存在不一致的。JDK1.2 之后,分配空间、对象初始化等等操作才都放到工作内存中进行了。由于 synchronized 关键字的关系,执行到语句 (2) 的时候,走出同步块时,JVM 会将主存和工作内存的 instance 引用的对象刷新到一致,即 instance 是 “可见” 的。但问题出在上面的 (1),没有 synchronized,也没有 volatile、final,没有人来保证调用 get 方法时获得的 n 是正确的值,即这个 n 未必是 “可见” 的。如果 n 比 instance 晚同步到主存,就存在一个时间间隙,这个间隙内获取到的 instance 是一个不健康的 instance,其中的 this.n 是取不到正确的 Number 对象的。

在 JSR133 中,对 JMM 做了一个修正,后引入或增强了 synchronized、volatile 和 final 关键字,通过它们的运用,上述问题能够得到解决。另外,还有一种解决的方法可以是使用静态的内部类:

代码语言:javascript
复制
private static class InnerInstance {  
   public static Instance instance = new Instance();  
}  
public static Instance getInstance() {  
   return InnerInstance.instance;  
}  

3、单例可以被继承,这是一个很大的好处,这便于用户 overwrite 其中的某方法,当然,继承单例的场景较少见;而静态类一般不被继承。关于单例的继承细节,这里暂不讨论,有几种办法,有兴趣的同学可以自行阅读 JDK 的 Calendar 类。

4、单例可以实现自某接口,可以继承自某类。静态类也可以继承自某类,但是就没法使用父类里面的 protect 成员了。推广来说,这一点和上一点都可以看做是面向对象带来的好处:封装、继承和多态,静态类不能很好地具备其中的后两点。

5、单例可以比较方便地扩展为有限实例。根据需要,我可以通过工厂,生产出两个内部状态不同的单例对象——这在静态类中是难以做到的。Spring 可以看做一系列大工厂,但其中的 bean 也只有 singleton 和 prototype 两种,生产不出 static 的新类型;当你的工具成为了对象,就能够保持良好的扩展性。

还有一个有趣的例子是 JDK 的 Calendar.getInstance() 方法,从方法看很像是获得一个单例,其实不是,每次都去创建了新的 Calendar 对象;同时,使用 abstract 修饰它自己,保证了无法使用 new 实例化,又开放了 getInstance 这样一个接口来获取默认实现,而获取的默认实现,又恰恰是 Calendar 的子类。这种形式可以看做是单例的一个变体。

6、有人说,单例在使用过程中申请的资源可以被及时释放并回收内存,但是静态类不行。这也是没有道理的,别忘了静态类也是可以存放状态的,在确定不再使用资源后,及时将资源的引用置为 null 就可以了。

7、如果希望在类加载的时候做复杂的操作,那么在静态类中,需要引入 static 块来初始化数据,如果期间抛出了异常,就可能发生一个 “ClassDefNotFoundError” 的诡异错误,这对问题定位是不利的。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

×Scan to share with WeChat

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档