设计模式
JAVA常见的几种设计模式解读
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
单例模式(Singleton Pattern)
懒汉式,线程不安全
1
2
3
4
5
6
7
8
9
10
11public class Singleton{
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}当多个线程并行调用getInstance()的时候,就会创建多个实例。在多线程下不能工作。
懒汉式,线程安全
为了解决上述问题,将整个getInstance()方法设置为同步(synchronized).
1
2
3
4
5
6public static synchronized Singleton getInstance(){
if(singleton == null){
instance = new Singleton();
}
return instance;
}虽然做到了线程安全,并且也解决了多实例问题,但是并不高效。因为在任何时候只能有一个线程调用getInstance()方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。
双重检验锁
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查
instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。1
2
3
4
5
6
7
8
9
10public static Singleton getSingleton(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}这段代码看起来很完美,很可惜,它是有问题。主要在于
instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
我们只需要将 instance 变量声明成 volatile 就可以了.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
饿汉式
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
1
2
3
4
5
6
7
8
9
10public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
静态内部类
1
2
3
4
5
6
7
8
9public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
枚举
1
2
3public enum EasySingleton{
INSTANCE;
}我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。
工厂模式(Factory Pattern)
工厂顾名思义就是创建产品,根据产品是具体产品还是具体工厂可分为简单工厂模式和工厂方法模式,根据工厂的抽象程度可分为工厂方法模式和抽象工厂模式。该模式用于封装和管理对象的创建,是一种创建型模式。
简单工厂模式
该模式对对象创建管理方式最为简单,因为其仅仅简单的对不同类对象的创建进行了一层薄薄的封装。该模式通过向工厂传递类型来指定要创建的对象,其UML类图如下:

使用手机生产来讲解该模式:
Phone类:手机标准规范类(AbstractProduct)
1
2
3public interface Phone {
void make();
}MiPhone类:制造小米手机(Product1)
1
2
3
4
5
6
7
8
9
10public class MiPhone implements Phone {
public MiPhone() {
this.make();
}
public void make() {
// TODO Auto-generated method stub
System.out.println("make xiaomi phone!");
}
}IPhone类:制造苹果手机(Product2)
1
2
3
4
5
6
7
8
9
10public class IPhone implements Phone {
public IPhone() {
this.make();
}
public void make() {
// TODO Auto-generated method stub
System.out.println("make iphone!");
}
}PhoneFactory类:手机工厂(Factory)
1
2
3
4
5
6
7
8
9
10
11public class PhoneFactory {
public Phone makePhone(String phoneType) {
if(phoneType.equalsIgnoreCase("MiPhone")){
return new MiPhone();
}
else if(phoneType.equalsIgnoreCase("iPhone")) {
return new IPhone();
}
return null;
}
}Demo类:
1
2
3
4
5
6
7public class Demo {
public static void main(String[] arg) {
PhoneFactory factory = new PhoneFactory();
Phone miPhone = factory.makePhone("MiPhone"); // make xiaomi phone!
IPhone iPhone = (IPhone)factory.makePhone("iPhone"); // make iphone!
}
}工厂方法模式
和简单工厂模式中工厂负责生产所有产品相比,工厂方法模式将生成具体产品的任务分发给具体的产品工厂,其UML类图如下:

也就是定义一个抽象工厂,其定义了产品的生产接口,但不负责具体的产品,将生产任务交给不同的派生类工厂。这样不用通过指定类型来创建对象了。
接下来继续使用生产手机的例子来讲解该模式。
其中和产品相关的Phone类、MiPhone类和IPhone类的定义不变。
AbstractFactory类:生产不同产品的工厂的抽象类
1
2
3public interface AbstractFactory {
Phone makePhone();
}XiaoMiFactory类:生产小米手机的工厂(ConcreteFactory1)
1
2
3
4
5
6public class XiaoMiFactory implements AbstractFactory{
public Phone makePhone() {
return new MiPhone();
}
}AppleFactory类:生产苹果手机的工厂(ConcreteFactory2)
1
2
3
4
5
6public class AppleFactory implements AbstractFactory {
public Phone makePhone() {
return new IPhone();
}
}演示:
1
2
3
4
5
6
7
8public class Demo {
public static void main(String[] arg) {
AbstractFactory miFactory = new XiaoMiFactory();
AbstractFactory appleFactory = new AppleFactory();
miFactory.makePhone(); // make xiaomi phone!
appleFactory.makePhone(); // make iphone!
}
}抽象工厂模式
上面两种模式不管工厂怎么拆分抽象,都只是针对一类产品Phone(AbstractProduct),如果要生成另一种产品PC,应该怎么表示呢?
最简单的方式是把2中介绍的工厂方法模式完全复制一份,不过这次生产的是PC。但同时也就意味着我们要完全复制和修改Phone生产管理的所有代码,显然这是一个笨办法,并不利于扩展和维护。
抽象工厂模式通过在AbstarctFactory中增加创建产品的接口,并在具体子工厂中实现新加产品的创建,当然前提是子工厂支持生产该产品。否则继承的这个接口可以什么也不干。
其UML类图如下:

从上面类图结构中可以清楚的看到如何在工厂方法模式中通过增加新产品接口来实现产品的增加的。
接下来我们继续通过小米和苹果产品生产的例子来解释该模式。
为了弄清楚上面的结构,我们使用具体的产品和工厂来表示上面的UML类图,能更加清晰的看出模式是如何演变的:

PC类:定义PC产品的接口(AbstractPC)
1 | public interface PC { |
MiPC类:定义小米电脑产品(MIPC)
1 | public class MiPC implements PC { |
MAC类:定义苹果电脑产品(MAC)
1 | public class MAC implements PC { |
下面需要修改工厂相关的类的定义:
AbstractFactory类:增加PC产品制造接口
1 | public interface AbstractFactory { |
XiaoMiFactory类:增加小米PC的制造(ConcreteFactory1)
1 | public class XiaoMiFactory implements AbstractFactory{ |
AppleFactory类:增加苹果PC的制造(ConcreteFactory2)
1 | public class AppleFactory implements AbstractFactory { |
演示:
1 | public class Demo { |
代理模式(Proxy Pattern)
代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。代理对象在客户端和目标对象之间起到中介作用,代理模式属于结构性设计模式。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象,下面是代理模式的结构类图:

为什么要用代理模式?
- 中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
- 开闭原则,增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
有哪几种代理模式?
我们有多种不同的方式来实现代理。如果按照代理创建的时期来进行分类的话, 可以分为两种:静态代理、动态代理。静态代理是由程序员创建或特定工具自动生成源代码,在对其编译。在程序员运行之前,代理类.class文件就已经被创建了。动态代理是在程序运行时通过反射机制动态创建的。
静态代理
创建服务类接口:
1
2
3public interface BuyHouse {
void buyHosue();
}实现服务接口:
1
2
3
4
5
6
7public class BuyHouseImpl implements BuyHouse {
public void buyHosue() {
System.out.println("我要买房");
}
}创建代理类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class BuyHouseProxy implements BuyHouse {
private BuyHouse buyHouse;
public BuyHouseProxy(final BuyHouse buyHouse) {
this.buyHouse = buyHouse;
}
public void buyHosue() {
System.out.println("买房前准备");
buyHouse.buyHosue();
System.out.println("买房后装修");
}
}编写测试类:
1
2
3
4
5
6
7
8public class ProxyTest {
public static void main(String[] args) {
BuyHouse buyHouse = new BuyHouseImpl();
buyHouse.buyHosue();
BuyHouseProxy buyHouseProxy = new BuyHouseProxy(buyHouse);
buyHouseProxy.buyHosue();
}
}总结:
优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
缺点:我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
动态代理
在动态代理中我们不再需要再手动的创建代理类,我们只需要编写一个动态处理器就可以了。真正的代理对象由JDK再运行时为我们动态的来创建。
编写动态处理器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class DynamicProxyHandler implements InvocationHandler {
private Object object;
public DynamicProxyHandler(final Object object) {
this.object = object;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("买房前准备");
Object result = method.invoke(object, args);
System.out.println("买房后装修");
return result;
}
}编写测试类:
1
2
3
4
5
6
7
8public class DynamicProxyTest {
public static void main(String[] args) {
BuyHouse buyHouse = new BuyHouseImpl();
BuyHouse proxyBuyHouse = (BuyHouse) Proxy.newProxyInstance(BuyHouse.class.getClassLoader(), new
Class[]{BuyHouse.class}, new DynamicProxyHandler(buyHouse));
proxyBuyHouse.buyHosue();
}
}注意*Proxy.newProxyInstance()*方法接受三个参数:
- *ClassLoader loader:*指定当前目标对象使用的类加载器,获取加载器的方法是固定的。
- *Class<?>[] interfaces:*指定目标对象实现的接口的类型,使用泛型方式确认类型。
- InvocationHandler:
指定``动态处理器,执行目标对象的方法时,会触发事件处理器的方法。
总结:
虽然相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫Proxy。Java的继承机制注定了这些动态代理类们无法实现对class的动态代理,原因是多继承在Java中本质上就行不通。有很多条理由,人们可以否定对 class代理的必要性,但是同样有一些理由,相信支持class动态代理会更美好。接口和类的划分,本就不是很明显,只是到了Java中才变得如此的细化。如果只从方法的声明及是否被定义来考量,有一种两者的混合体,它的名字叫抽象类。实现对抽象类的动态代理,相信也有其内在的价值。此外,还有一些历史遗留的类,它们将因为没有实现任何接口而从此与动态代理永世无缘。如此种种,不得不说是一个小小的遗憾。但是,不完美并不等于不伟大,伟大是一种本质,Java动态代理就是佐例。
CGLIB代理
JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
创建CGLIB代理类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class CglibProxy implements MethodInterceptor {
private Object target;
public Object getInstance(final Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("买房前准备");
Object result = methodProxy.invoke(object, args);
System.out.println("买房后装修");
return result;
}
}创建测试类:
1
2
3
4
5
6
7
8public class CglibProxyTest {
public static void main(String[] args){
BuyHouse buyHouse = new BuyHouseImpl();
CglibProxy cglibProxy = new CglibProxy();
BuyHouseImpl buyHouseCglibProxy = (BuyHouseImpl) cglibProxy.getInstance(buyHouse);
buyHouseCglibProxy.buyHosue();
}
}总结:
CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。
装饰者模式(Decorator Pattern)
动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。
适用性:
当采用类继承的方式会造成类爆炸的情况。如本文的例子中,基本饮料(被装饰者)可能有茶、水、牛奶等等、可以添加的(装饰者)有糖、果肉、珍珠等。
如果通过继承的方式,为每一种类型的奶茶设计一种类会造成类爆炸,同时也不利于后期的扩展(如又添加一种基本饮料豆浆的情况),此时通过装饰者模式可以很好的解决问题。
装饰者模式本质是一种组合的思想(不同于继承的思想),多组合少继承。利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态地进行扩展。
装饰者模式的主要组成部分:抽象被装饰者、具体被装饰者、抽象装饰者、具体装饰者。(当只有一个具体被装饰者、一个具体装饰者,其对应的抽象类可以省略)。

装饰者模式要点:
- 多种具体被装饰者(主体类)抽象出一个抽象被装饰类,后面通过多态,动态传递具体对象。
- 抽象装饰类继承抽象被装饰者(保持接口);要求传入被装饰者(使用父类应用、protected修饰)。
- 多种具体装饰者抽象出一个抽象装饰类。
- 具体装饰者中,可以添加新方法,可以重写方法;需要使用被装饰者方法的地方用传入的被装饰者引用。
被装饰抽象类-基本饮料:
1 | /* |
具体的被装饰者类-water类:
1 | /* |
具体的被装饰者类-Tea类:
1 | /* |
抽象装饰者类-Decorator:
1 | /* |
装饰者类-果肉:
1 | public class FleshDecorator extends Decorator{ |
装饰者类-糖:
1 | public class SugarDecorator extends Decorator{ |
测试用例:
1 | public class TestCase { |
适配器模式(Adapter Pattern)
适配器模式定义:将一个类的接口,转化成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间。
适配器的例子
适配器模式的对象:
- 请求对象(手机)
- 适配器对象(带有两孔和三孔的插座)
- 需要适配的对象(三孔插座)
- 请求对象所需要的接口(插座要有两孔)
手机类:
1 | public class Mobile |
两孔插座接口:
1 | public interface ITwoHole |
三孔插座:
1 | public class ThreeHole |
插座需要有两孔:
1 | public class LineWithTwoHole implements ITwoHole |
测试:
1 | public class Program { |
适配器模式类图

适配器模式的关系:请求对象引用需要适配的接口,适配器引用需要适配对象,适配器需要通过被适配对象来实现需要适配的接口。
外观模式(Facade Pattern)
外观模式定义:外观模式提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。
Computer类:
1 | public class Computer |
Light类:
1 | public class Light |
Print类:
1 | public class Print |
Cinema类:
1 | public class Cinema |
接口类:
1 | public interface ITakePicture |
接口实现类:
1 | public class TakePicture implements ITakePicture |
测试类:
1 | public class Program { |
可以看出外观模式的好处是:简化了设备的接口,同时降低了客户端和各个设备的耦合。例如,照相机的照相方法改成了TakePhoto(),那么也只需要在外观的实现方法中修改该设备的方法即可,不用去修改客户端代码。