一、简述

静态方法是属于类(class)的,普通方法属于实体对象(也就是 new 出来的对象)。Spring 注入是在容器中实例化对象,所以不能使用静态方法。Springframework 里,不能@Autowired一个静态变量,使之成为一个 Spring bean。例如下面这样:

@Autowired  
private static OurBean ourBean;

ourBean 在这种状态下不能够被依赖注入,会抛出java.lang.NullPointerException。因为静态变量/类变量不是对象的属性,而是一个类的属性,Spring 是基于对象层面上的依赖注入。

使用静态变量/类变量扩大了静态方法的使用范围。静态方法在 Spring 是不推荐使用的。依赖注入的主要目的,是让容器去产生一个对象的实例。然后在整个生命周期中使用它们,同时也让工作更加容易。

一旦使用静态方法,就不再需要去产生这个类的实例。同时也不能为该类,依靠注入方式去产生多个具有不同的依赖环境的实例。这种 static field 是隐含共享的,并且是一种 global 全局状态,Spring 同样不推荐这样去做。

编程中最常用的模式就是单例模式了,然而单例模式都用在什么场合?为什么不用静态方法而要用单例模式呢?要搞清这些问题,需要从静态方法和非静态方法的区别和联系说起。

二、静态方法常驻内存,非静态方法只有使用的时候才分配内存?错

一般都认为是这样,并且怕静态方法占用过多内存而建议使用非静态方法,其实这个理解是错误的。为什么会这样,先从内存分配开始说起:

托管堆的定义:对于 32 位的应用程序来说,应用程序完成进程初始化后, CLR 将在进程的可用地址空间分配一块保留的地址空间,它是进程(每个进程可使用4GB)中可用地址空间上的一块内存区域,但并不对应任何物理内存,这块地址空间即是托管堆。

托管堆有分为多个区域,其中最重要的是垃圾回收堆(GC Heap)和加载堆(Loader Heap)。GC Heap 用于存储对象实例,受 GC 管理;Loader Heap 又分为 High-Frequency Heap、Low-Frequency Heap 和 Stub Heap,不同的堆上又存储不同的信息。Loader Heap 最重要的信息就是元数据相关的信息,也就是 Type 对象,每个 Type 在 Loader Heap 上体现为一个 Method Table(方法表),而 Method Table 中则记录了存储的元数据信息,例如基类型、静态字段、实现的接口、所有的方法等等。Loader Heap 不受 GC 控制,其生命周期为从创建到 AppDomain 卸载。(摘自《你必须知道的.Net》)

由此,静态方法和非静态方法,在内存里其实都放在 Method Table 里了,在一个类第一次被加载的时候,它会在 Loader Heap 里把静态方法,非静态方法都写入 Method Table 中,而且 Loader Heap 不受 GC 控制,所以一旦加载,GC 就不会回收,直到 AppDomain 卸载。

当然,静态方法和非静态方法,它们都是在第一次加载后就常驻内存,所以方法本身在内存里,没有什么区别。所以也就不存在“静态方法常驻内存,非静态方法只有使用的时候才分配内存”这个结论了。

三、静态方法和非静态方法的区别

在内存中的区别是,非静态方法在创建实例对象时,因为属性的值对于每个对象都各不相同,因此在 new 一个实例时,会把该实例属性在 GC Heap 里拷贝一份,同时这个 new 出来的对象放在堆栈上,堆栈指针指向了刚才拷贝的那一份实例的内存地址上。而静态方法则不需要,因为静态方法里面的静态字段,就是保存在 Method Table 里了,只有一份。

因此静态方法和非静态方法,在调用速度上,静态方法速度一定会快点,因为非静态方法需要实例化,分配内存,但静态方法不用。这种速度上差异可以忽略不计。

四、为什么要有非静态方法

早期的结构化编程,几乎所有的方法都是“静态方法”,引入实例化方法概念是面向对象概念出现以后的事情了,区分静态方法和实例化方法不能单单从性能上去理解,创建 c++、Java 和 c# 这样面向对象语言的大师引入实例化方法一定不是要解决什么性能、内存的问题,而是为了让开发更加模式化、面向对象化。这样说的话,静态方法和实例化方式的区分是为了解决模式的问题。
接下来继续思考,如果全部用静态方法,不用非静态方法,不是一样能实现功能吗?没错,但是这样代码是基于对象,而不是面向对象的,因为面向对象的继承和多态,都是非静态方法。
第二个原因,多线程的情况下,如果静态方法使用了一个静态字段,这个静态字段可以会被多个线程修改,因此说如果在静态方法里使用了静态变量,这就会有线程安全问题。当然了,就算不是多线程,因为静态字段只有一份,同样会有被其他地方修改的问题。

五、总结

1️⃣什么时候用静态方法,什么时候使用非静态方法

既然静态方法和实例化方式的区分是为了解决模式的问题,如果考虑不需要继承和多态的时候,就可以使用静态方法,但就算不考虑继承和多态,就一概使用静态方法也不是好的编程思想。
从另一个角度考虑,如果一个方法和所在类的实例对象无关,那么它就应该是静态的,否则就应该是非静态。因此像工具类,一般都是静态的。

2️⃣为什么使用单例模式而不用静态方法

从面向对象的角度讲:
虽然都能实现目的,但是它们一个是基于对象,一个是面向对象的。如果一个方法和它所在类的实例对象无关,那么它就应该是静态的,反之它就应该是非静态的。如果确实应该使用非静态的方法,但是在创建类时又确实只需要维护一份实例时,就需要用单例模式了。
比如说在系统运行时候,就需要加载一些配置和属性,这些配置和属性是一定存在了,又是公共的,同时需要在整个生命周期中都存在,所以只需要一份就行。这个时候就需要单例模式或静态方法去维持一份且仅这一份拷贝,但此时这些配置和属性又是通过面向对象的编码方式得到的,就应该使用单例模式,或者不是面向对象的,但它本身的属性应该是面向对象的,虽然使用静态方法能同样解决问题,但是最好的解决方案也应该是使用单例模式。

从功能上讲:
单例模式可以控制单例数量;可以进行有意义的派生;对实例的创建有更自由的控制。

3️⃣数据库连接能不能做 Singleton?

如果是简单地把一个 connection 对象封存在单例对象中,这样是错误的。因为连接池里有多个链接可以用,如果使用 Singleton,那在 WEB 访问时,就只能用一个数据库链接,那不是死的很惨?

但是链接池可以使用单例模式,初始化的时候创建比如 100 个 connection 对象,然后在需要的时候提供一个,用过之后返回到 pool 中。用单例模式,是保证连接池有且只有一个。

再举个例子,比如 DAO 层写好一个调用数据库表的类,在 Service 层应用此类时,如果每次都 new 创建的话需要频繁的创建和回收,而 DAO 层这个类里又没有和对象相关的值变量,所以不需要每次都 new 一个,此时就可以用单例模式来创建这个 DAO 实例。