到底是用"静态类"还是单例

5,857 阅读6分钟

更新了一个疑惑,请盆友们不吝赐教,见第四点。


这里把都是静态成员的类叫做静态类,区别于静态内部类这个概念。

疑问来自写各个 Util、Manager、Helper 的时候,到底是应该写成拥有自己成员变量成员方法的单例模式,还是把所有的成员都做成静态。这应该是个经典的论题,根据社区和博客以及个人的理解,我想至少在我自己这里终结问题下的很多子问题,下面是归纳和一些疑惑思考。

  1. 没有高低之分,只有使用场景的差别

  2. 比较他们的时候总是有人说单例可以懒加载。所谓单例的延迟加载、懒加载,跟谁比延迟了?延迟的是什么?是如何延迟的?

    • 是单例模式的懒汉实现(Double Check,静态内部类,枚举)和饿汉实现比,延迟了

    • 延迟的是 new 这个单例的带来的开销(我理解下来,这个开销不只是空间,时间也有)

    • 这里就要说道说道了,很多博客里根本没有弄清楚前因后果就搬砖式的 Copy 一堆陈词滥调放在文章里。之所以要延迟加载,是想根据业务、场景需要,在第一次用到单例的时候才去实例化它,也就是把创建实例的开销往后挪了。但是实际上,饿汉式本身就是延迟加载的,我们看一下饿汉式的写法

      public class SampleClass {
          private static SampleClass sInst = new SampleClass();
          public static SampleClass getInst() {
              return sInst;
          }
      }
      

      Line 2 中 sInst 的创建是在什么时候执行的呢?是在类加载的最后一步“初始化”中执行的,而初始化这一步是条件的!当且仅当 JVM [^注1]规定的六种条件下才会去初始化,分别是

      1. 创建类的实例

      2. 访问类或接口的静态变量(特例:如果是用 static final 修饰的常量,那就不会对类进行显式初始化。static final 修饰的变量则会做显式初始化)

      3. 调用类的静态方法

      4. 反射(Class.forName(packagename.className))

      5. 初始化类的子类。注:子类初始化问题,只有父类访问子类中的静态变量、方法,子类才会初始化;否则仅父类初始化。

      6. jvm 启动时被标明为启动类的类

      所以饿汉式本来就自带延迟加载的属性,只有在访问 Line 3 中 getInst 方法才会去初始化 sInst 并创建实例(满足了初始化的条件3)。那搞个懒汉式出来干啥呢,是因为上面这句话是有前提条件的,如果在 getInst 之前访问了别的静态成员,或者用了反射等等其他满足初始化的条件,类就提前加载了,导致实例化提前,违背了需求。因此,为了达到延迟实例化[^注2]的目的,有两种方法。其一就是换成懒汉式的写法,不将单例的创建放在静态成员的初始化中,避免了类加载时机带来的影响。其二是仍然采用饿汉式,不过从设计上避免类的提前加载:*比如有观点认为,从代码设计的角度看,单例的 Class 本来就不该拥有除了 getInst 方法以外任何能被访问的静态成员,Kotlin 就认为,你定义在 object 内部的变量都是和 object 有关的,而且 Kotlin 中不需要懒汉式的写法,因为这门语言不存在上述提前加载的问题。

    说了这么多关于单例模式,那么静态类呢。其实两者无从比较,因为静态类并不会去堆中实例化一个对象,如果只考虑堆内存的占用的话,单例在静态类面前并没有所谓延迟加载的优势。

  3. 两者的性能问题。而从内存占用来讲,静态方法和实例方法没有区别!无论哪个实例,静不静态,方法只有一个,JVM 会在方法区保存方法相关的信息,时机也没有区别。而静态变量和实例变量只是分配的位置不同,前者在方法区,后者在堆中,并且因为类的生命周期是伴随 JVM 的,前者永驻方法区,后者因为其所属实例是静态的,被类持有,所以在堆中也用远不会被回收。从访问速度来讲,两者并无明显差别(本人木有做过验证),但我有个小猜想,*类对象是分配在堆中的,它是方法区的入口,当通过它直接访问方法区的静态成员和通过它访问方法区的单例再通过单例访问堆中的实例成员,这个速度怕还是应该有区别的吧。*很多同学在讨论这个问题时所说的单例的优缺点,根本不是相较与静态类来说的,而是相较于非单例来说的,什么单例节省资源咯、单例就是个内存泄漏咯。

  4. 从编程思想上,单例的出现并不是为了解决什么高深复杂的性能之类的问题,它只是面向对象的一种体现,可以继承拓展,可以实现接口,比较灵活(场景见得少,我对这个理解不深,有个例子是 Java.getRuntime 方法会根据 JVM 的不同,返回不同的实例)。静态类更像是一个方法的集合,提供全局的访问而已, 比如大多数的 Util。静态类不适合需要维护状态的情形和比如对于资源的访问。我看到一句话总结的很好,但我暂时只有感性的理解囧,可能是代码读的少。

    静态方法用来执行无状态的一个完整操作,实例方法则相反,它通常是一个完整逻辑的一部分,并且需要维护一定的状态值.

    现在理解下来,状态可以简单的认为就是需要维护的属性,如果方法没有对属性的写操作,那就说是无状态的。**那为什么静态方法不适合做有状态的操作呢?**Google 了一下,很多博客都是搬的一句话,说静态方法在维护状态方面会导致许多狡猾的 BUG(黑人问号),不加以同步机制会存在竞态冲突。但是单例模式维护状态难道就不需要同步了??请大家不吝赐教

  5. 测试的问题, Mock。这条暂时没有去了解。


很多自己的理解,如有疏漏错误,恳请补充指正。原文链接

[^注1]: 文章里的内容都基于 JVM,更为严谨的做法应该是基于 DVM ,后续有时间会去调研一哈在类加载这个事情上 DVM 有木有什么大的改动 [^注2]: 我个人觉得这个事情本来就应该叫做延迟实例化,而不应该叫做延迟加载。加载会与类加载搞混淆,类的加载(加载、链接、初始化这个大过程)是我们不可控的。