`

再议单例模式和静态类

阅读更多

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

 

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

 

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

 

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

 

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

 

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

 

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关键字,通过它们的运用,上述问题能够得到解决。另外,还有一种解决的方法可以是使用静态的内部类:

 

    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”的诡异错误,这对问题定位是不利的。

 

文章系本人原创,转载请注明出处和作者

 

5
0
分享到:
评论
8 楼 gaoke_71 2014-07-28  
多数不大明白
7 楼 yanqingluo 2014-01-25  
补充一下刚才我说的,单例类不应该再被继承,但是单例类是可以有父类。
6 楼 yanqingluo 2014-01-25  
我觉得calender类不是单例,它每次getInstance都NEW 了个新对象。

我分析了一下,是因为他NEW具体对象的构造方法并不是PRIVATE的。

个人意见:单例是不应该被继承的,因为,他的构造函数如果是私有的,无法被其他人调用,继承它没有意义。

5 楼 yanqingluo 2014-01-25  
好文章。转走了。
4 楼 RayChase 2012-04-07  
该用户名已经存在 写道
以前都没有思考过单例和静态类的问题,知道前不久看到一个帖子回复里一哥们问:有了静态类,我们为什么还要用单例?
贴子回复也众说纷纭,但是居然没有一个我觉得满意的答案。我也傻乎乎的疑惑在这个问题上。
不过通过一直一来的思考和火哥的文章,我可以用一句最简单的话解释了:
因为Java是面向对象的编程,因为要面向接口,因为要多态,而静态类完全违背了这些,静态类是纯面向过程的方式(其实就相当于库函数),适当的写点工具类还可以,要想游刃有余的面向对象编程,恐怕静态类就成为绊脚石了。

二者具体的不同包括我文中提到的那些,而不只是你说的这一点。
事实上,即便是单例模型,也不是纯粹意义上的面向对象;不过另一方面,也不应当过分追求面向对象的设计。终究要选择合适的工具和手段。
3 楼 该用户名已经存在 2012-04-06  
以前都没有思考过单例和静态类的问题,知道前不久看到一个帖子回复里一哥们问:有了静态类,我们为什么还要用单例?
贴子回复也众说纷纭,但是居然没有一个我觉得满意的答案。我也傻乎乎的疑惑在这个问题上。
不过通过一直一来的思考和火哥的文章,我可以用一句最简单的话解释了:
因为Java是面向对象的编程,因为要面向接口,因为要多态,而静态类完全违背了这些,静态类是纯面向过程的方式(其实就相当于库函数),适当的写点工具类还可以,要想游刃有余的面向对象编程,恐怕静态类就成为绊脚石了。
2 楼 RayChase 2012-03-31  
啸笑天 写道
枚举实现单例
public enum Singleton {
    INSTANCE {
        public void doSomeMethod() {
            // . . .
        }
    };
    protected abstract void doSomeMethod();
}

Good! 
1 楼 啸笑天 2012-03-31  
枚举实现单例
public enum Singleton {
    INSTANCE {
        public void doSomeMethod() {
            // . . .
        }
    };
    protected abstract void doSomeMethod();
}

相关推荐

    joomla里面的单例模式和纯静态类

    在Joomla!涉及到了很多的单例模式,比如JFactory,JURI等等。 对于一个请求中需要一个对象实例的,joomla大多采用了单例模式,可以避免重复实例化带来的资源浪费和性能损耗。

    Java中的单例模式与静态类

    单例模式与静态类(一个类,所有方法为静态方法)是另一个非常有趣的问题,在《Java中有关单例模式的面试问题》博文中露掉了,由于单例模式和静态类都具有良好的访问性,它们之间有许多相似之处,例如,两者可以直接...

    Java单例模式实现静态内部类方法示例

    主要介绍了Java单例模式实现静态内部类方法示例,涉及构造函数私有化等相关内容,需要的朋友可以了解下。

    单例模式的多种实现.docx

    单例模式的七种实现方法以及分析,可以作文大作业提交 ...3.6使用静态内部类实现单例模式 12 3.7使用枚举类实现单例模式 13 4.单例模式怎么用才合理? 14 4.1测试 14 4.2无边界 15 5.设计模式学习总结: 15

    单例模式中声明静态自己类型的指针编译显示未定义处理

    主要解决在单例模式下类中声明静态指针存储单例对象,在.cpp文件中编译时显示未定义的解决办法: 参考博客: http://bbs.csdn.net/topics/10439749

    java单例模式实例

    一个简单的java工程,包含注释,一目了然,其中包含了单例模式的所有实现方式,懒汉式,饿汉式,双重校验,枚举,静态内部类等方式实现单例。

    php单例模式实例

    如果说php是一门面向对象编程的话,那么设计模式就是它的灵魂,其中单例模式就是设计模式的重中之重了,分享鄙人一直用的单例模式。

    php基于单例模式封装mysql类

    介绍了php基于单例模式封装mysql类,结合完整实例形式分析了php使用单例模式封装的mysql类定义与使用方法 掌握满足单例模式的必要条件 (1)私有的构造方法-为了防止在类外使用new关键字实例化对象 (2)私有的成员属性...

    php基于单例模式封装mysql类.zip

    介绍了php基于单例模式封装mysql类,结合完整实例形式分析了php使用单例模式封装的mysql类定义与使用方法 掌握满足单例模式的必要条件 (1)私有的构造方法-为了防止在类外使用new关键字实例化...

    java实现单例模式-静态内部类模式(线程安全)

    静态内部类模式结合了懒汉模式和饿汉模式的优点,既实现了延迟加载,又保证了线程安全。

    第5天static静态关键字和单例模式.pptx

    static静态关键字和单例模式

    java 设计模式 单例模式

    //单例模式,始终只产生一个对象 /*public class SingleTest { public static void main(String[] args) { SingleTon singleTon1=SingleTon.getInstance(); SingleTon singleTon2=SingleTon.getInstance(); ...

    单例模式 工厂模式DEMO

    1、单例模式 结论:单例模式可以把数据封装在对象里,操作对象;而静态类是把数据封装在类里,操作类。单例模式中,数据只有在对象存在的时候才能操作;而静态类中,数据可以直接通过静态类点出来操作。 2、工厂模式...

    单例模式PHP实现代码类.zip

    介绍一个单例模式PHP实现代码类,getInstance()方法必须设置为公有的,必须调用此方法,对象方法不能访问普通的对象属性,所以$_instance需要设为静态的

    Java设计模式之单例模式讲解

    入名所示,该文件为最详细的Java单例模式讲解并附有讲解代码。主要讲了单例模式的几种方法,懒汉...饿汉模式和静态内部类模式如何设置能够避免使用反射方法获取多个实列,以及实现了序列化的类如何避免创建多个实列。

    Java实现多种单例模式

    通过Java语言,主要实现了六种单例的生成方法,包括懒汉式、饿汉式、双重校验锁、枚举、静态内部类,可以根据实际情况选择使用

    这可能是最全的单例模式了

    静态内部类实现单例模式5. 饿汉实现单例模式6. 饿汉变种实现单例模式7. 枚举实现单例模式static修饰下是怎么做到线程安全的?完全不使用synchronized实现单例模式1. CAS(AtomicReference)实现单例模式2. ...

    单例模式简单实现

    实现单例模式:1、私有化的构造函数。2、提供静态方法或者公共属性提供全局访问点。3、考虑多线程高并发的问题。 注意点:lock之前判断一下实例是否为空。lock之前判断一下什么情况可以lock什么情况不可以,一般多...

    android也架构之单例模式

    单例模式中有区分了懒汉式和饿汉式,懒汉式主要是用时间来换空间,饿汉式则是用空间来换时间。饿汉式是线程安全的,懒汉式是非线程安全的,如果要实现懒汉式的非线程安全,则可以再访问点添加synchronized关键字声明...

    Java-设计模式-单例模式-实现源码(简单实现、双重检查锁、静态内部类、枚举类)

    Java-设计模式-单例模式-实现源码(简单实现、双重检查锁、静态内部类、枚举类)

Global site tag (gtag.js) - Google Analytics