`

再议单例模式和静态类

阅读更多

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

相关推荐

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

    在Java中,单例模式可以通过多种方式实现,包括懒汉式、饿汉式、双重检查锁定和静态内部类方法等。今天,我们主要介绍了Java单例模式实现静态内部类方法示例,涉及构造函数私有化等相关内容。 单例模式的定义 单例...

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

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

    设计模式单例模式和工厂模式综合应用

    **代码**文件则提供了实际的Java实现,通过查看这些代码,我们可以看到单例模式和工厂模式如何在实践中被运用,包括类的定义、方法的实现以及如何通过代码调用来创建和管理对象。 总的来说,这个项目为学习和理解...

    Java 单例模式 工具类

    通过`SingletonFactory`工具类,我们不仅可以方便地使用各种单例模式,还可以对自定义的单例进行统一管理和访问,提高了代码的可维护性和灵活性。在实际开发中,应根据项目需求选择适合的单例实现方式,以保证代码的...

    c++单例模式线程日志类

    在这个特定的场景中,我们讨论的是一个实现了单例模式的日志类,该类专为多线程环境设计,具备日志等级控制、精确的时间戳以及可变长参数和标准格式化输出的功能。 首先,让我们深入了解单例模式。单例模式的主要...

    设计模式之单例模式(结合工厂模式)

    此外,单例模式还有几种变体,比如静态内部类单例和枚举单例。静态内部类单例利用Java类加载机制保证了线程安全,而枚举单例则是Java中实现单例的最佳方式,因为它天然支持序列化且防止反射攻击。 在代码实现上,...

    Java中的单例模式与静态类

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

    43丨单例模式(下):如何设计实现一个集群环境下的分布式单例模式?1

    在单例模式中,类的构造函数是私有的,防止外部直接创建对象,而是通过静态方法获取该类的唯一实例。单例模式的唯一性通常是在进程范围内,即在同一个进程中,无论何时调用单例类的获取实例方法,都会返回相同的对象...

    php单例模式实例

    单例模式的核心思想是限制类的实例化过程,确保在程序运行期间,类的实例只有一个。通过控制类的构造函数,使其不能被外部直接实例化,而是通过一个静态方法来获取唯一的实例。这样,无论何时何地,只要调用这个静态...

    C#单例模式详解 C#单例模式详解C#单例模式详解

    - 难以解耦,单例模式使得依赖它的类紧密耦合,不易于重构和扩展。 在实际开发中,应谨慎使用单例模式,避免过度使用导致代码维护难度增加。在某些场景下,如依赖注入和微服务架构中,可能需要避免使用单例,以保持...

    单例模式,single

    - **序列化的影响**:如果单例类实现了 `Serializable` 接口,那么通过序列化和反序列化可能会产生新的实例,这同样会破坏单例模式。 针对上述问题,可以采取以下几种策略: 1. **线程安全的单例模式**:可以通过...

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

    - 反对依赖注入:由于单例模式控制了实例化过程,使得外部难以替换或模拟这个类的实例,不利于单元测试和解耦。 - 延迟加载:单例模式往往在第一次调用`getInstance()`时就创建了实例,可能导致不必要的内存占用。...

    java单例模式实例

    单例模式是软件设计模式中的一种经典模式,用于确保一个类只有一个实例,并提供一个全局访问点。在Java中,有多种实现单例模式的方法,每种都有其特点和适用场景。接下来,我们将深入探讨这些实现方式。 首先,我们...

    单例模式和工厂模式代码

    在Java中,实现单例模式有多种方法,包括懒汉式(线程不安全)、饿汉式(静态常量)、双检锁(DCL)和枚举单例。其中,双检锁和枚举单例是线程安全的,推荐在多线程环境下使用。 ```java // 双检锁/双重校验锁(DCL...

    使用C++11实现线程安全的单例模式

    在C++编程中,单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。线程安全的单例模式在多线程环境下尤其重要,因为不正确的实现可能导致多个线程创建多个实例,这违反了单例模式...

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

    单例模式是软件设计模式中的一种,用于控制类的实例化过程,确保一个类只有一个实例,并提供一个全局访问点。这种模式在系统中需要频繁创建和销毁的对象,或者需要共享资源的情况下非常有用。然而,实现单例模式时,...

    设计模式 中的 单例模式和观察者模式

    单例模式是一种确保一个类只有一个实例,并提供全局访问点的设计模式。这种模式在资源管理、缓存、对话框、注册表设置、日志记录等场景中非常有用。为了实现单例,通常我们会创建一个私有的构造函数,防止直接实例化...

    php单例模式和工厂模式

    单例模式是一种设计模式,它保证了一个类只有一个实例,并提供一个全局访问点。这种模式在 PHP 中尤其适用于控制共享资源,例如数据库连接、缓存服务或者配置对象,确保在整个应用程序中这些资源只被初始化一次。 ...

    单例模式_命令模式

    单例模式的关键在于限制类的实例化过程,通常通过私有构造函数和静态工厂方法来实现。这样可以避免多个实例导致的数据不一致性和资源浪费。 命令模式则将请求封装为一个对象,使得可以使用不同的请求、队列请求、...

Global site tag (gtag.js) - Google Analytics