再议单例模式和静态类

2022-07-15 20:14:40 浏览数 (1)

单例模式还是静态类,这是一个老话题了,从我刚开始接触 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

0 人点赞