设计模式——单例模式

适用于只需要一个唯一对象时。

分为饿汉模式和懒汉模式两种。
饿汉模式,系统开始运行时就生成唯一一个静态对象。
懒汉模式,系统开始运行时不生成静态对象,直到需要时才生成该唯一对象。
推荐使用饿汉模式。懒汉模式有线程风险。

饿汉模式

1
2
3
4
5
6
7
8
public class SingleSample {
private SingleSample(){}
private static SingleSample singleSample = new SingleSample();

public static SingleSample getSingleSample() {
return singleSample;
}
}

懒汉模式

1
2
3
4
5
6
7
8
9
10
11
public class SingleSample2 {
private SingleSample2(){}
private static SingleSample2 singleSample2;

public static SingleSample2 getSingleSample2() {
if(null == singleSample2){
singleSample2 = new SingleSample2();
}
return singleSample2;
}
}

总之,单例模式是一种非常简单的设计模式。靠私有化构造方法禁止new对象,其唯一的对象是一个静态对象。

单例模式的使用场景 [1]

  • 要求生成唯一序列号的环境;
  • 在整个项目中需要一个共享访问点或共享数据,例如在一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
  • 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
  • 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然也可以直接声明为static的方式)。

单例模式的扩展

有上限的多例模式是单例模式的扩展。只需要一个对象时用单例模式即可,如果只需要5个对象呢?用一个静态对象列表就可以了,如ArrayList。

总结回顾

  • 防止被 new ====> 构造方法私有化
  • 保证唯一 ====> 使用静态对象

两私有一公开(私有构造方法、私有静态实例(懒实例化/直接实例化)、公开静态获取方法)

枚举实现单例

上述示例,涉及线程安全问题(即使有多重检查锁也可以通过反射破坏单例)。
枚举是实现单例模式的最佳方法。
《Effective Java》:“使用enum关键字来实现单例模式的好处是这样非常简洁,并且无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候(JVM保证)。”

枚举可以有自己的属性和方法,实质是功能齐全的类。正因为枚举本身是单例的,所以两个枚举值作比较可以直接用 == 而不是 equals() 方法,事实上,Enum类重写了 equals() 方法,也是用 == 来比较的:

1
2
3
public final boolean equals(Object other){
return this==other;
}

枚举单例模式示例:

1
2
3
4
5
6
7
8
9
10
11
12
public enum Singleton {
AAA;
public void getA(){
System.out.println("AAAAAA");
}
}
public class TestS {
public static void main(String[] args) {
Singleton s = Singleton.AAA;
s.getA();
}
}

*枚举和类

  • 声明关键字和类不同,enum / class
  • 在声明任何类成员(或初始化块)之前,枚举必须首先声明其所有的枚举常量。
  • 枚举的构造器只能是private,默认也是private。
  • 枚举不能声明为扩展自另一个类型,所有的枚举都隐式地扩展自java.lang.Enum(但任何类都不能直接扩展Enum)。所有其它类型(包括另一个枚举)都不能扩展枚举,因为所有的枚举类型的行为就好像它们隐式是final一样。枚举不能声明为 abstract 的,但枚举可以声明抽象方法。
  • 枚举可以声明它实现了一个或多个接口。事实上,所有的枚举都隐式是Serializable和Comparable。
  • 枚举类型不允许覆盖Object的finalize方法,因为枚举实例永远都不能被终结。
  • 每个枚举都具有两个静态方法,是由编译器自动生成的。
    • public static E[] values()
    • public static E[] valueOf(String name)
  • 枚举也不能显示地声明为final的,尽管它的行为看起来就是final的。

警惕单例模式的滥用 [2]

单例模式是最基本的设计模式之一,也是程序员最喜欢用的设计模式之一,面试的时候也经常遇到。在先前的项目中,我非常喜欢使用单例模式,只要我认为同一个进程中,某个类只应该有一份唯一的实例,那么我就会毫不犹豫地使用单例模式。但是最近在实践中也慢慢发现单例模式的一些问题,暂且记下:
第一,由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。另外类的构造函数通常也是私有的,所以无法被继承。尤其在单元测试的时候,我们常常需要继承原始的类,并覆写一些方法以达到打桩的目的。
第二,对需要多例的集成测试不友好。虽然类A在正常情况下,一个进程中只应该有一个实例,但是在集成测试的时候,我们可能需要在同一个进程里构造出两个A的实例,以方便测试。
第三,代码模块之间的依赖不清晰。举例,当模块B需要使用类A的实例,它通常可以A.getInstance()来获取A的唯一实例,这样会造成整个项目代码中,到处都有A.getInstance()这样的使用,于是很难看出到底哪些模块真正依赖A。而如果B的构造函数是B(A a),那么就可以很直观地看出B对A的依赖。

[1] 摘自《设计模式之禅(第2版)》
[2] 摘自阿涵-_-的博客