当前位置 : 首页 » 文章分类 :  开发  »  面试准备11-设计模式

面试准备11-设计模式

Java面试准备之设计模式


设计模式六大原则

单一职责原则告诉我们实现类要职责单一;
里氏替换原则告诉我们不要破坏继承体系;
依赖倒置原则告诉我们要面向接口编程;
接口隔离原则告诉我们在设计接口的时候要精简单一;
迪米特法则告诉我们要降低耦合。
而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

设计模式六大原则 - 很全很详细
http://www.uml.org.cn/sjms/201211023.asp

单一职责原则SRP

单一职责原则(Single Responsibility Principle, SRP)
定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。

问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。

解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

遵循单一职责原的优点有:

  • 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
  • 提高类的可读性,提高系统的可维护性;
  • 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。


里氏替换原则LSP

里氏代换原则(Liskov Substitution Principle, LSP)

定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。
核心思想:在使用基类的的地方可以任意使用其子类,能保证子类完美替换基类。

这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。
定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2:所有引用基类的地方必须能透明地使用其子类的对象。

问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能

当使用继承时,应遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

需注意:如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系 采用依赖、聚合、组合等关系代替继承。

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

比如java中的子类覆盖父类方法时,如有异常,子类中抛出的异常与父类中一致或是其子类型,即 子类抛出的异常类型不能比父类抛出的异常类型更宽泛。
这也是 里氏替换原则 的一个体现,就是子类不能突然抛出一个父类中都没规定的异常,否则会让使用者无法处理。

设计模式六大原则(2):里氏替换原则
https://blog.csdn.net/zhengzhb/article/details/7281833


依赖倒置原则DIP

依赖倒置原则(Dependence Inversion Principle, DIP)

定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
高层模块就是调用端,低层模块就是具体实现类。抽象就是指接口或抽象类。细节就是实现类。

问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

依赖倒置原则的核心思想是面向接口编程

在实际编程中,我们一般需要做到如下3点:

  • 低层模块尽量都要有抽象类或接口,或者两者都有。
  • 变量的声明类型尽量是抽象类或接口。
  • 使用继承时遵循里氏替换原则。

接口隔离原则ISP

接口隔离原则(Interface Segregation Principle, ISP)

定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。


未遵循接口隔离原则的设计
上图中,类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法。

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用

接口隔离原则跟单一职责原则的异同:
其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。
其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。


迪米特法则LoD

迪米特法则(Law of Demeter, LoD)
定义:一个对象应该对其他对象保持最少的了解。或者,一个软件实体应当尽可能少地与其他实体发生相互作用。
迪米特法则来自于1987年美国东北大学(Northeastern University)一个名为“Demeter”的研究项目。迪米特法则又称为最少知识原则(Least Knowledge Principle,LKP),

问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

解决方案:尽量降低类与类之间的耦合。

迪米特法则还有一个更简单的定义:只与直接的朋友通信
首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部

迪米特法则首先强调在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限,也就是说,不需要让别的类知道的字段和方法就不要设为public

在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;

迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度


开发封闭原则OCP

开放封闭原则(Open Close Principle, OCP)

定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。

开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。


组合/聚合复用原则(CARP)

组合/聚合复用原则(Composite/Aggregate Reuse Principle)

组合/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分; 新的对象通过向这些对象的委派达到复用已有功能的目的。
也就是,要尽量使用类的合成复用,尽量不要使用继承。

组合/聚合复用原则的含义是,如果只是达到代码复用的目的,尽量使用组合与聚合,而不是继承。因为继承的耦合性更大,组合聚合只是引用其他的类的方法,而不会受引用的类的继承而改变血统。

组合/聚合复用原则可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

下面这篇文章,介绍 Java 中的 Stack 类和 Vector 类关系时也提到了尽量使用组合而不是继承。
Java 程序员,别用 Stack?!
https://mp.weixin.qq.com/s/Ba8jrULf8NJbENK6WGrVWg


领域驱动设计DDD

贫血模型与充血模型

贫血病导致的失忆症

具体参考笔记 领域驱动设计


实践

Java中使用了哪些设计模式?

单例模式

Math类中包了一个Random单例(静态内部类)

Math 类中包了一个 Random 类的单例,使用了静态内部类的单例实现方式,属于饿汉模式,其实 Java 中的好多单例都用了静态内部类的形式,这也是目前比较流行的很简单清晰的单例实现方式。

public final class Math {
  private static final class RandomNumberGeneratorHolder {
      static final Random randomNumberGenerator = new Random();
  }
  public static double random() {
      return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
  }
}

Runtime 类是单例(饿汉模式)

由于任何进程只会运行于一个虚拟机实例当中,所以在 Runtime 中采用了单例模式,即只会产生一个虚拟机实例:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
      return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    ...
}

模板方法模式

Java 中的 AQS 使用了模板方法模式

抽象类 AbstractQueuedSynchronizer 中定义了 acquire() 方法的步骤:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

但却没定义第一步中的 tryAcquire() 方法的具体实现,需要每个实现了 AbstractQueuedSynchronizer 的具体类来实现
比如 ReentrantLock 的内部类 FairSync 继承自 AbstractQueuedSynchronizer 并实现了 tryAcquire 方法。

Java 中的 ThreadLocal 使用了模板方法模式

ThreadLocal 中的 initialValue() 方法是 protected 的,目的就是为了让子类覆盖。

protected T initialValue() {
    return null;
}

Spring中使用了哪些设计模式?

适配器模式

HandlerInterceptorAdapter 拦截器适配器

Spring 的 HandlerInterceptor 中定义了三个拦截方法 preHandle, postHandle, afterCompletion
有时候我们可能只需要实现三个回调方法中的某一个,如果实现 HandlerInterceptor 接口的话,三个方法必须实现,不管你需不需要,此时spring提供了一个HandlerInterceptorAdapter适配器(一种适配器设计模式的实现),允许我们只实现需要的回调方法。


MyBatis中使用了哪些设计模式?

工厂模式 SqlSessionFactory

在Mybatis中比如SqlSessionFactory使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂模式。
DefaultSqlSessionFactory 根据传入的不同参数构建 SqlSession 对象
TransactionFactory, ManagedTransactionFactory, JdbcTransactionFactory 根据不同参数构建 Transaction

建造者模式 SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 类根据不同的输入参数来构建 SqlSessionFactory 这个工厂对象

代理模式 MapperProxy

代理模式可以认为是 Mybatis 的核心使用的模式,正是由于这个模式,我们只需要编写 Mapper.java 接口,不需要实现,由Mybatis帮我们完成具体SQL的执行。

通过这种方式,我们只需要编写Mapper.java接口类,当真正执行一个Mapper接口的时候,就会转发给 MapperProxy.invoke 方法,而该方法则会调用后续的 sqlSession.curd -> executor.execute -> prepareStatement 等一系列方法,完成SQL的执行和返回。

装饰器模式 Cache

在mybatis中,缓存的功能由根接口 Cache(org.apache.ibatis.cache.Cache)定义。
整个体系采用装饰器设计模式,数据存储和缓存的基本功能由 PerpetualCache(org.apache.ibatis.cache.impl.PerpetualCache)永久缓存实现,然后通过一系列的装饰器来对PerpetualCache永久缓存进行缓存策略等方便的控制。

用于装饰 PerpetualCache 的标准装饰器共有8个(全部在org.apache.ibatis.cache.decorators包中):
FifoCache:先进先出算法,缓存回收策略
LoggingCache:输出缓存命中的日志信息
LruCache:最近最少使用算法,缓存回收策略
ScheduledCache:调度缓存,负责定时清空缓存
SerializedCache:缓存序列化和反序列化存储
SoftCache:基于软引用实现的缓存管理策略
SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问
WeakCache:基于弱引用实现的缓存管理策略
另外,还有一个特殊的装饰器TransactionalCache:事务性的缓存

正如大多数持久层框架一样,mybatis缓存同样分为一级缓存和二级缓存

一级缓存,又叫本地缓存,是PerpetualCache类型的永久缓存,保存在执行器中(BaseExecutor),而执行器又在SqlSession(DefaultSqlSession)中,所以一级缓存的生命周期与SqlSession是相同的。
二级缓存,又叫自定义缓存,实现了Cache接口的类都可以作为二级缓存,所以可配置如encache等的第三方缓存。二级缓存以namespace名称空间为其唯一标识,被保存在Configuration核心配置对象中。
二级缓存对象的默认类型为PerpetualCache,如果配置的缓存是默认类型,则mybatis会根据配

MyBatis 开启二级缓存后,在一级缓存处理前,用 CachingExecutor 装饰了 BaseExecutor 的子类,在委托具体职责给 delegate 之前,实现了二级缓存的查询和写入功能。

// 装饰器 CachingExecutor
public class CachingExecutor implements Executor {
  // 内部含有一个 被装饰者的实例
  private final Executor delegate;
}

Mybatis源码解读-设计模式总结
https://blog.csdn.net/qq_35807136/article/details/79931345

Mybatis源码解读-设计模式总结
http://www.crazyant.net/2022.html


常用设计模式

随笔分类 - 设计模式
http://www.cnblogs.com/xiaoxi/category/1004551.html

工厂模式

简单工厂模式(静态工厂模式)

一个工厂类,根据传入的不同参数生成各种不同class的对象。这些对象同属于某一基类或接口。

问题:
增加新产品时要修改工厂类(修改if else或switch case),扩展性不好。不符合开闭原则。
优秀的java代码是符合“开放-封闭”原则(Open-Closed Principe, OCP)的,也就是说对扩展开发,对修改关闭

工厂方法模式

多个工厂类,按需选择一工厂生成对象。这些工厂之间靠拥有同一接口方法关联,所以叫“工厂方法”模式。
不在工厂里直接创建对象,而是直接封装一个一个的小工厂,每个工厂负责创建自己的子类

问题:
符合开闭原则。
但是这还是有缺点的,如果产品类过多,我们就要生成很多的工厂类。

抽象工厂模式

多个工厂类,每个工厂类可以生产不同的产品对象。
抽象工厂是应对产品族概念的。比如说,每个汽车公司可能要同时生产轿车,货车,客车,那么每一个工厂都要有创建轿车,货车和客车的方法。

结合实例分析简单工厂模式 工厂方法模式 抽象工厂模式的区别
https://www.cnblogs.com/zhi-hao/p/4028169.html

为什么使用工厂模式?(优点)

首先,工厂模式是为了解耦:把对象的创建和使用的过程分开。就是Class A 想调用 Class B ,那么A只是调用B的方法,而至于B的实例化,就交给工厂类。

其次,工厂模式可以降低代码重复。如果创建对象B的过程都很复杂,需要一定的代码量,而且很多地方都要用到,那么就会有很多的重复代码。我们可以这些创建对象B的代码放到工厂里统一管理。既减少了重复代码,也方便以后对B的创建过程的修改维护。
由于创建过程都由工厂统一管理,所以发生业务逻辑变化,不需要找到所有需要创建B的地方去逐个修正,只需要在工厂里修改即可,降低维护成本。

举个例子:
一个数据库工厂:可以返回一个数据库实例,可以是mysql,oracle等。
这个工厂就可以把数据库连接需要的用户名,地址,密码等封装好,直接返回对应的数据库对象就好。不需要调用者自己初始化,减少了写错密码等等这些错误。调用者只负责使用,不需要管怎么去创建、初始化对象。


装饰模式

装饰模式,顾名思义,就是对已经存在的某些类进行装饰,以此来扩展一些功能。

涉及角色:
Component 为统一接口,也是装饰类和被装饰类的基本类型。
ConcreteComponent 为具体实现类,也是被装饰类,他本身是个具有一些功能的完整的类。
Decorator 是装饰类,实现了 Component 接口的同时还在内部维护了一个 ConcreteComponent 的实例,并可以通过构造函数初始化。而 Decorator 本身,通常采用默认实现,他的存在仅仅是一个声明:我要生产出一些用于装饰的子类了。而其子类才是赋有具体装饰效果的装饰产品类。
ConcreteDecorator 是具体的装饰产品类,每一种装饰产品都具有特定的装饰效果。可以通过构造器声明装饰哪种类型的ConcreteComponent,从而对其进行装饰。

装饰器模式实现:
装饰者和被装饰者需要继承同一个接口或者是抽象类,被装饰者作为装饰者的一个变量。程序中原来调用被装饰者某方法func1的地方改成调用装饰者相同的那个方法func1,并且装饰者的该方法func1上添加了一些额外的功能,在方法func1中再调用被装饰着的方法func1。

装饰器的价值在于装饰,他并不影响被装饰类本身的核心功能。装饰器模式在不影响各个ConcreteComponent核心价值的同时,添加了他特有的装饰效果,具备非常好的通用性,这也是他存在的最大价值。

装饰器模式(Decorator)——深入理解与实战应用
https://www.cnblogs.com/jzb-blog/p/6717349.html

Java IO设计模式(装饰模式与适配器模式)
https://www.cnblogs.com/wangrd/p/7152662.html

适配器模式,装饰模式,代理模式区别

一句话,装饰者模式是增强对象功能(穿不同的衣服),代理模式是控制代理的对象,但不对其做功能增加

装饰器与适配器都有一个别名叫做 包装模式(Wrapper),它们看似都是起到包装一个类或对象的作用,但是使用它们的目的很不一样。
适配器模式的意义是要将一个接口转变成另一个接口,它的目的是通过改变接口来达到重复使用的目的。目的是 兼容
装饰器模式不是要改变被装饰对象的接口,而是恰恰要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方式而提升性能。目的是 增强
代理模式,同一个类而去调用另一个类的方法,不对这个方法进行直接操作。目的是 隐藏

Java IO中的装饰模式(InputStream-FilterInputStream)

装饰模式在Java语言中的最著名的应用莫过于Java I/O标准库的设计了。

由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现。而如果采用装饰模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰模式是Java I/O库的基本模式。

抽象构件(Component)角色:由InputStream扮演。这是一个抽象类,为各种子类型提供统一的接口。
具体构件(ConcreteComponent)角色:由ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream等类扮演。它们实现了抽象构件角色所规定的接口。
抽象装饰(Decorator)角色:由FilterInputStream扮演。它实现了InputStream所规定的接口。
具体装饰(ConcreteDecorator)角色:由几个类扮演,分别是BufferedInputStream、DataInputStream以及两个不常用到的类LineNumberInputStream、PushbackInputStream。

即具体构件角色是节点流,装饰角色是过滤流。

InputStream这个抽象类有一个子类与上述其它子类非常不同,这个子类就是FilterInputStream,可参见上图中的InputStream族谱图。

翻开FilterInputStream的代码,我们可以看到,它内部又维护了一个InputStream的成员对象,并且它的所有方法,都是调用这个成员对象的同名方法。
换句话说,FilterInputStream它什么事都不做。就是把调用委托给内部的InputStream成员对象。

FilterInputStream的又有其子类,分别是:BufferedInputStream, DataInputStream, LineNumberInputStream, PushbackInputStream

虽然从上面代码看FilterInputStream并没有做什么有卵用的事,但是它的子类可不同了,以BufferedInputStream为例,这个类提供了提前读取数据的功能,也就是缓冲的功能。
从这里可以开始感受到,BufferedInputStream就是一个装饰者,它能为一个原本没有缓冲功能的InputStream添加上缓冲的功能。

比如我们常用的FileInputStream,它并没有缓冲功能,我们每次调用read,都会向操作系统发起调用索要数据。假如我们通过BufferedInputStream来装饰它,那么每次调用read,会预先向操作系统多拿一些数据,这样就不知不觉中提高了程序的性能。

同理,对于其它的FilterInputStream的子类,其作用也是一样的,那就是装饰一个InputStream,为它添加它原本不具有的功能。OutputStream以及家属对于装饰器模式的体现,也以此类推。

Java IO : 流,以及装饰器模式在其上的运用
https://segmentfault.com/a/1190000004255439

《java与设计模式》之装饰模式详解&Java IO中的装饰器模式
https://blog.csdn.net/caihuangshi/article/details/51334097


适配器模式

适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

适配器模式的用途
用电器做例子,笔记本电脑的插头一般都是三相的,即除了阳极、阴极外,还有一个地极。而有些地方的电源插座却只有两极,没有地极。电源插座与笔记本电脑的电源插头不匹配使得笔记本电脑无法使用。这时候一个三相到两相的转换器(适配器)就能解决此问题,而这正像是本模式所做的事情。

模式所涉及的角色有:

  • 目标(Target)角色:这就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因此目标不可以是类。
  • 源(Adaptee)角色:现在需要适配的接口。
  • 适配器(Adaper)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。

类适配器

适配器类继承Adaptee类,又实现Target接口,对于Adaptee中已有的方法,默认调用父类(也就是Adaptee类)的方法,对于Adaptee中没有而Target中有的方法,需要额外定义来实现。

对象适配器

对象适配器相当于对源Adaptee的代理,对象适配器内含一个Adaptee的引用,对于Adaptee中已有的方法直接委托给Adaptee对象来调用,对于Adaptee中没有而Target中有的方法,需要额外定义来实现。

建议尽量使用对象适配器的实现方式,多用合成/聚合、少用继承。当然,具体问题具体分析,根据需要来选用实现方式,最适合的才是最好的。

最小接口原则

先来说一下最小接口原则,这个原则所表达的思想是说接口的行为应该尽量的少,如果没做到这个原则,就会出现实现这个接口的子类,很可能出现很多方法是空着的情况,因为你的接口设计的过大,导致接口中原本不该出现的方法出现了,结果现在子类根本用不上这个方法,但由于JAVA语言规则的原因,实现一个接口必须实现它的全部方法,所以我们的子类不得不被迫写一堆空方法在那,只为了编译通过。

缺省适配器

缺省适配(Default Adapter)模式为一个接口提供缺省实现,这样子类型可以从这个缺省实现进行扩展,而不必从原有接口进行扩展。

在很多情况下,必须让一个具体类实现某一个接口,但是这个类又用不到接口所规定的所有的方法。缺省适配模式可以很好的处理这一情况。可以设计一个抽象的适配器类实现接口,此抽象类要给接口所要求的每一种方法都提供一个空的方法。

这种情况其实蛮多的,因为接口设计的最小化只是理想状态,难免会有一些实现类,对其中某些方法不感兴趣,这时候,如果方法过多,子类也很多,并且子类的大部分方法都是空着的,那么就可以采取这种方式了。

使用适配器模式克服观察者模式的缺点

观察者模式的一个缺点,即如果一个现有的类没有实现Observer接口,那么我们就无法将这个类作为观察者加入到被观察者的观察者列表中

举个例子,比如我们希望将HashMap这个类加到观察者列表里,在被观察者产生变化时,假设我们要清空整个map。但是Observable的观察者列表只认识Observer这个接口,它不认识HashMap,这种情况下,我们就可以使用类适配器的方式将我们的HashMap进行改造,类适配器采用继承的方式,那么我们写出如下适配器。

public class HashMapObserverAdapter<K, V> extends HashMap<K, V> implements Observer {
    public void update(Observable o, Object arg) {
        //被观察者变化时,清空Map
        super.clear();
    }
}

即我们继承我们希望复用其功能的类,并且实现我们想适配的接口,在这里就是Observer,那么就会产生一个适配器,这个适配器具有原有类(即HashMap)的功能,又具有观察者接口,所以这个适配器现在可以加入到观察者列表了。

设计模式学习之适配器模式
https://blog.csdn.net/u012124438/article/details/70486083

《JAVA与模式》之适配器模式
http://www.cnblogs.com/java-my-life/archive/2012/04/13/2442795.html


代理模式

代理模式包含如下角色:
ISubject:抽象主题角色,是一个接口。该接口是对象和它的代理共用的接口。
RealSubject:真实主题角色,是实现抽象主题接口的类。
Proxy:代理角色,内部含有对真实对象RealSubject的引用,从而可以操作真实对象。代理对象提供与真实对象相同的接口,以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装。

静态代理

静态代理的实现:
代理类和被代理类实现相同的接口,代理类持有被代理类的引用,代理方法执行时,同步调用被代理类的相同方法。

静态代理是死的,不会在运行时动态创建,相当于在编译期,就给被代理的对象生成了一个不可动态改变的代理类。

Java设计模式——代理模式实现及原理
https://blog.csdn.net/goskalrie/article/details/52458773

设计模式学习之代理模式
https://blog.csdn.net/u012124438/article/details/70184219


策略模式

定义:策略模式定义了一系列的算法,并将每一个算法封装起来,而且使他们可以相互替换,让算法独立于使用它的客户而独立变化。

分析下定义,策略模式定义和封装了一系列的算法,它们是可以相互替换的,也就是说它们具有共性,而它们的共性就体现在策略接口的行为上,另外为了达到最后一句话的目的,也就是说让算法独立于使用它的客户而独立变化,我们需要让客户端依赖于策略接口。

这个模式涉及到三个角色:

  • 抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
  • 具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。
  • 上下文(Context)角色:持有一个 Strategy 的引用。

策略模式的优点
(1)策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免代码重复。
(2)使用策略模式可以避免使用多重条件(if-else)语句。多重条件语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重条件语句里面,比使用继承的办法还要原始和落后。

策略模式的缺点
(1)客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道算法或行为的情况。
(2)由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。

基础的策略模式实现

// 抽象策略接口
interface Strategy {
    void doAction();
}

// 具体策略A
class ConcreteStrategyA implements Strategy {
    @Override
    public void doAction() {
        System.out.println("Concrete Strategy A");
    }
}

// 具体策略B
class ConcreteStrategyB implements Strategy {
    @Override
    public void doAction() {
        System.out.println("Concrete Strategy B");
    }
}

// 基本的策略上下文
class OriginStrategyContext {
    private Strategy strategy;

    OriginStrategyContext(Strategy strategy) {
        this.strategy = strategy;
    }

    public void doAction() {
        strategy.doAction();
    }
}

// 基础的策略模式
@Test
public void testOriginStrategyContext() {
    new OriginStrategyContext(new ConcreteStrategyA()).doAction();
    new OriginStrategyContext(new ConcreteStrategyB()).doAction();
}

结合Spring容器实现策略模式(干掉if else代码)

Strategy 抽象策略接口
ConcreteStrategyA 具体策略A,通过 @Component 被扫描到 Spring 容器中
ConcreteStrategyB 具体策略B,通过 @Component 被扫描到 Spring 容器中
StrategyContext 策略上下文,实现了 Spring 的 ApplicationContextAware 来拿到 ApplicationContext 实例,实现根据 策略 Bean 名字获取 Spring 上下文容器中的 具体策略实例

@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
public class StrategyTest {
    @Autowired
    private StrategyContext strategyContext;

    // 策略模式与简单工厂模式结合,通过Spring上下文实现
    @Test
    public void testStrategyPattern() {
        // 根据策略名字选择具体策略,无 if else 代码
        strategyContext.getStrategy("concreteStrategyA").doAction();
        strategyContext.getStrategy("concreteStrategyB").doAction();
    }
}

// 抽象策略接口
interface Strategy {
    void doAction();
}

// 具体策略A
@Component
class ConcreteStrategyA implements Strategy {
    @Override
    public void doAction() {
        System.out.println("Concrete Strategy A");
    }
}

// 具体策略B
@Component
class ConcreteStrategyB implements Strategy {
    @Override
    public void doAction() {
        System.out.println("Concrete Strategy B");
    }
}

// 策略上下文与简单工厂模式结合,通过Spring上下文实现
@Component
class StrategyContext implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    // 根据策略 Bean 名字获取具体策略实例
    public Strategy getStrategy(String strategyName) {
        return (Strategy) applicationContext.getBean(strategyName);
    }
}

利用策略模式优化过多 if else 代码
https://juejin.im/post/5c5172d15188256a2334a15d

Spring Boot中如何干掉if else
https://juejin.im/post/5c551122e51d457fcc5a9790

还在使用if else写代码?试试 “策略模式” 吧! - 程序员小灰
https://blog.csdn.net/bjweimengshu/article/details/105803438

设计模式学习之策略模式
https://blog.csdn.net/u012124438/article/details/70039943


模板方法模式

定义一个模板结构,将具体内容延迟到子类去实现。

在面向对象开发过程中,通常我们会遇到这样的一个问题:我们知道一个算法所需的关键步骤,并确定了这些步骤的执行顺序。但是某些步骤的具体实现是未知的,或者说某些步骤的实现与具体的环境相关。
例子1:银行业务办理流程
在银行办理业务时,一般都包含几个基本固定步骤:
取号排队->办理具体业务->对银行工作人员进行评分。
取号取号排队和对银行工作人员进行评分业务逻辑是一样的。但是办理具体业务是个不相同的,具体业务可能取款、存款或者转账。

模板方法:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 T模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

1)模板方法模式是基于继承的代码复用基本技术,模板方法模式的结构和用法也是面向对象设计的核心之一。在模板方法模式中,可以将相同的代码放在父类中,而将不同的方法实现放在不同的子类中。
2)在模板方法模式中,我们需要准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来让子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现,这就是模板方法模式的用意。模板方法模式体现了面向对象的诸多重要思想,是一种使用频率较高的模式。

JDK 中的 AQS 使用了模板方法模式

抽象类 AbstractQueuedSynchronizer 中定义了 acquire() 方法的步骤:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

但却没定义第一步中的 tryAcquire() 方法的具体实现,需要每个实现了 AbstractQueuedSynchronizer 的具体类来实现,比如 ReentrantLock 的内部类 FairSync 继承自 AbstractQueuedSynchronizer 并实现了 tryAcquire 方法。

Feign签名拦截器中使用模板方法模式

如下是一个 feign 签名拦截器的代码片段, SignInterceptor 抽象类实现了 feign 的 RequestInterceptor 接口,是一个拦截器,在 HTTP 请求发送前会被应用。
SignInterceptor 定义了计算签名的总体流程,包括:
1、判断是否需要签名 notSign()
2、签名前动作 preSign()
3、根据 RequestTemplate 计算签名 sign()
4、签名后动作 postSign()
上面的3个动作都有对应的抽象方法,开放给具体的实现类去实现,抽象类中只规定了这几个步骤的执行顺序,handler() 用来返回处理器实例,也需要子类去实现。

public abstract class SignInterceptor implements RequestInterceptor {

    abstract void preSign(RequestTemplate template);

    abstract void postSign(RequestTemplate template, String sign);

    abstract boolean notSign(RequestTemplate template);

    abstract SignHandler handler();

    @Override
    public void apply(RequestTemplate template) {
        if (notSign(template))
            return;
        preSign(template);
        String sign = sign(template);
        postSign(template, sign);
    }

    private String sign(RequestTemplate template) {
        return handler().generateSign(template);
    }
}

来看一个具体的实现类

public class ParamInterceptor extends SignInterceptor {
    @Autowired
    @Qualifier("ParamSignHandler")
    private SignHandler paramSignHandler;

    @Override
    public void preSign(RequestTemplate template) {
        template.query("lang", "zh");
        template.query("timestamp", new Date().getTime() / 1000L);
        template.query("app_id", "10010");
    }

    @Override
    public void postSign(RequestTemplate template, String sign) {
        template.query("sign", sign);
    }

    @Override
    public boolean notSign(RequestTemplate template) {
        return false;
    }

    @Override
    public SignHandler handler() {
        return paramSignHandler;
    }
}

状态模式

状态模式:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

在很多情况下,一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,这样的对象叫做有状态的(stateful)对象,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。

模式的组成
环境类(Context): 定义客户感兴趣的接口。维护一个ConcreteState子类的实例,这个实例定义当前状态。
抽象状态类(State): 定义一个接口以封装与Context的一个特定状态相关的行为。
具体状态类(ConcreteState): 每一子类实现一个与Context的一个状态相关的行为。

状态模式中有什么优点呢?
首先是避免了过多的swith…case 或者if..else 语句的使用,避免了程序的复杂性;其次是很好的使用体现了开闭原则和单一职责原则,每个状态都是一个子类,你要增加状态就增加子类,你要修改状态,你只修改一个子类就可以了;最后一个好处就是封装性非常好,这也是状态模式的基本要求,状态变换放置到了类的内部来实现,外部的调用不用知道类内部如何实现状态和行为的变换。

状态模式既然有优点,那当然有缺点了,只有一个缺点,子类会太多,也就是类膨胀,你想一个事物有七八、十来个状态也不稀奇,如果完全使用状态模式就会有太多的子类,不好管理,这个需要大家在项目自己衡量。

Java设计模式——状态模式(STATE PATTERN)
https://blog.csdn.net/u012401711/article/details/52675873

策略模式和状态模式的区别

应用场景(目的)却不一样,State模式重在强调对象内部状态的变化改变对象的行为,Strategy模式重在外部对策略的选择,策略的选择由外部条件决定,也就是说算法的动态的切换。
但由于它们的结构是如此的相似,我们可以认为“状态模式是完全封装且自修改的策略模式”。即状态模式是封装对象内部的状态的,而策略模式是封装算法族的

设计模式 ( 十七) 状态模式State(对象行为型)
https://blog.csdn.net/hguisu/article/details/7557252


迭代器模式

迭代器模式:提供一种方法顺序的访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。

聚集类:Aggregate(抽象类)和ConcreteAggregate(具体聚集类)表示聚集类,是用来存储迭代器的数据。
在Aggregate(抽象类)中有一个CreateIterator方法,用来获取迭代器
迭代器:迭代器用来为聚集类提供服务,提供了一系列访问聚集类对象元素的方法。

迭代器是可以从前往后,或者从后往前遍历的。
为遍历不同聚集结构提供如:开始,下一个,是否有下一个,是否结束,当前哪一个等等的一个统一接口。

迭代器模式(Iterator)
https://www.cnblogs.com/cxxjohnson/p/6403851.html


观察者模式

观察者向被观察者上注册,当被观察者状态发生改变时(有事件发生时),所有观察者都得到通知。

观察者模式实例

JAVA设计模式之观察者模式
https://www.cnblogs.com/luohanguo/p/7825656.html

实例:有一个微信公众号服务,不定时发布一些消息,关注公众号就可以收到推送消息,取消关注就收不到推送消息。
1、定义一个抽象被观察者接口

package com.jstao.observer;

//抽象被观察者接口,声明了添加、删除、通知观察者方法
public interface Observerable {
    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    public void notifyObserver();
}

2、定义一个抽象观察者接口

package com.jstao.observer;

//抽象观察者,定义了一个update()方法,当被观察者调用notifyObservers()方法时,观察者的update()方法会被回调。
public interface Observer {
    public void update(String message);
}

3、定义被观察者,实现了Observerable接口,对Observerable接口的三个方法进行了具体实现,同时有一个List集合,用以保存注册的观察者,等需要通知观察者时,遍历该集合即可。

package com.jstao.observer;

import java.util.ArrayList;
import java.util.List;

//被观察者,也就是微信公众号服务,实现了Observerable接口,对Observerable接口的三个方法进行了具体实现
public class WechatServer implements Observerable {
    //注意到这个List集合的泛型参数为Observer接口,设计原则:面向接口编程而不是面向实现编程
    private List<Observer> list;
    private String message;

    public WechatServer() {
        list = new ArrayList<Observer>();
    }

    @Override
    public void registerObserver(Observer o) {
        list.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        if(!list.isEmpty())
            list.remove(o);
    }

    //遍历
    @Override
    public void notifyObserver() {
        for(int i = 0; i < list.size(); i++) {
            Observer oserver = list.get(i);
            oserver.update(message);
        }
    }

    public void setInfomation(String s) {
        this.message = s;
        System.out.println("微信服务更新消息: " + s);
        //消息更新,通知所有观察者
        notifyObserver();
    }
}

4、定义具体观察者,微信公众号的具体观察者为用户User

package com.jstao.observer;

//观察者,实现了update方法
public class User implements Observer {

    private String name;
    private String message;

    public User(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        this.message = message;
        read();
    }

    public void read() {
        System.out.println(name + " 收到推送消息: " + message);
    }

}

5、编写一个测试类

package com.jstao.observer;

public class Test {
    public static void main(String[] args) {
        WechatServer server = new WechatServer();

        Observer userZhang = new User("ZhangSan");
        Observer userLi = new User("LiSi");
        Observer userWang = new User("WangWu");

        server.registerObserver(userZhang);
        server.registerObserver(userLi);
        server.registerObserver(userWang);
        server.setInfomation("PHP是世界上最好用的语言!");

        System.out.println("----------------------------------------------");
        server.removeObserver(userZhang);
        server.setInfomation("JAVA是世界上最好用的语言!");

    }
}

推模式和拉模式

观察者模式在关于目标角色、观察者角色通信的具体实现中,有两个版本。一种情况便是目标角色在发生变化后,仅仅告诉观察者角色“我变化了”;观察者角色如果想要知道具体的变化细节,则就要自己从目标角色的接口中得到。这种模式被很形象的称为:拉模式——就是说变化的信息是观察者角色主动从目标角色中“拉”出来的。

还有一种方法,那就是我目标角色“服务一条龙”,通知你发生变化的同时,通过一个参数将变化的细节传递到观察者角色中去。这就是“推模式”——管你要不要,先给你啦。

这两种模式的使用,取决于系统设计时的需要。如果目标角色比较复杂,并且观察者角色进行更新时必须得到一些具体变化的信息,则“推模式”比较合适。如果目标角色比较简单,则“拉模式”就很合适啦。

深入浅出观察者模式
https://blog.csdn.net/ai92/article/details/375691

面试被问设计模式?不要怕看这里:观察者模式
http://blog.jobbole.com/109845/

Java内置观察者模式

在JAVA语言的java.util库里面,提供了一个Observable类以及一个Observer接口,构成JAVA语言对观察者模式的支持。

使用javaAPI的观察者模式需要明白这么几件事情:

  • 如何使对象变为观察者?
    实现观察者接口(java.util.Observer),然后调用Observable对象的addObserver()方法.不想再当观察者时,调用deleteObserver()就可以了.

  • 被观察者(主题)如何发出通知?
    第一步:先调用setChanged()方法,标识状态已经改变的事实.
    第二步:调用notifyObservers()方法或者notifyObservers(Object arg),这就牵扯到推(push)和拉(pull)的方式传送数据.如果想用push的方式”推”数据给观察者,可以把数据当做数据对象传送给notifyObservers(Object arg)方法,其中的arg可以为任意对象,意思是你可以将任意对象传送给每一个观察者。如果调用不带参数的notifyObserver()方法,则意味着你要使用pull的方式去主题对象中”拉”来所需要的数据.

  • 观察者如何接收通知?
    观察者只需要实现一个update(Observable o,Object arg)方法,第一个参数o,是指定通知是由哪个主题下达的,第二个参数arg就是上面notifyObserver(Object arg)里传入的数据,如果不传该值,arg为null.

《JAVA与模式》之观察者模式
http://www.cnblogs.com/java-my-life/archive/2012/05/16/2502279.html

设计模式–观察者模式初探和java Observable模式
https://www.cnblogs.com/fingerboy/p/5468994.html


单例模式

基本的实现思路
单例模式要求类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称)。

单例的实现主要是通过以下两个步骤:
1、将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
2、在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

懒汉模式和饿汉模式

单例模式可以分为懒汉式和饿汉式:
懒汉式单例模式:懒汉式就是不在类加载时就创建类的单例,而是在第一次使用实例的时候再创建。
饿汉式单例模式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。

单例模式的线程安全性

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

实现单例模式的8中方法

单例模式的八种写法比较
https://www.cnblogs.com/zhaoyan001/p/6365064.html

1、饿汉式(静态常量)[可用]

public class Singleton {
    private final static Singleton INSTANCE = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return INSTANCE;
    }
}

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

2、饿汉式(静态代码块)[可用]

public class Singleton {
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    private Singleton() {}
    public Singleton getInstance() {
        return instance;
    }
}

这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。

3、懒汉式(线程不安全)[不可用]

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

4、懒汉式(线程安全,同步方法)[不推荐用]

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。
缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

5、懒汉式(线程安全,同步代码块)[不可用]

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

由于第四种实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

6、双重检查[推荐用]

public class Singleton {
    private static volatile Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。

优点:线程安全;延迟加载;效率较高。

双重检查时的实例对象为什么必须是volatile的?

注意:使用双重检查时实例对象必须是volatile的,否则还会有问题。

这里涉及到了JVM编译器的指令重排。
指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址

但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象

假如singleton不是volatile的,线程A加锁进入代码块准备创建单例对象,当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。

为了避免这一情况呢,我们需要在instance对象前面增加一个修饰符volatile
volatile修饰符阻止了变量访问前后的指令重排,保证了指令执行顺序
如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。

漫画:什么是单例模式?(整合版) - 程序员小灰
https://blog.csdn.net/bjweimengshu/article/details/78716839

单例模式与双重检测
http://www.iteye.com/topic/652440

7、静态内部类[推荐用]

public class Singleton {
    private Singleton() {}
    private static class SingletonInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstanceHolder.INSTANCE;
    }
}

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:避免了线程不安全,延迟加载,效率高。

可能会有这样的疑问, SingletonInstanceHolder 中的 INSTANCE 是 private 的,为什么外部的 Singleton 类直接就能访问呢?
其实这是个 Java 编译器实现的语法糖。

8、枚举(防止反序列化重建单例)[推荐用]

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。可能是因为枚举在JDK1.5中才添加,所以在实际项目开发中,很少见人这么写过。

这种方式是Effective Java作者Josh Bloch提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。

利用反射构造多个单例对象

代码可以简单归纳为三个步骤:
第一步,获得单例类的构造器。
第二步,把构造器设置为可访问。
第三步,使用newInstance方法构造对象。

//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2)); //false

反序列化对单例模式的破坏

使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。

对于其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法。

漫画:什么是单例模式?(整合版) - 程序员小灰
https://blog.csdn.net/bjweimengshu/article/details/78716839

设计模式:单例模式
http://www.cnblogs.com/xiaoxi/p/7799456.html

常见的几种单例模式
https://www.cnblogs.com/Ycheng/p/7169381.html

面试被问设计模式?不要怕看这里:单例模式
http://blog.jobbole.com/109449/


上一篇 面试准备12-计算机基础

下一篇 面试准备10-消息中间件

阅读
评论
15,683
阅读预计57分钟
创建日期 2018-05-22
修改日期 2020-04-30
类别
目录
  1. 设计模式六大原则
    1. 单一职责原则SRP
    2. 里氏替换原则LSP
    3. 依赖倒置原则DIP
    4. 接口隔离原则ISP
    5. 迪米特法则LoD
    6. 开发封闭原则OCP
    7. 组合/聚合复用原则(CARP)
  2. 领域驱动设计DDD
    1. 贫血模型与充血模型
    2. 贫血病导致的失忆症
  3. 实践
    1. Java中使用了哪些设计模式?
      1. 单例模式
        1. Math类中包了一个Random单例(静态内部类)
        2. Runtime 类是单例(饿汉模式)
      2. 模板方法模式
        1. Java 中的 AQS 使用了模板方法模式
        2. Java 中的 ThreadLocal 使用了模板方法模式
    2. Spring中使用了哪些设计模式?
      1. 适配器模式
        1. HandlerInterceptorAdapter 拦截器适配器
    3. MyBatis中使用了哪些设计模式?
      1. 工厂模式 SqlSessionFactory
      2. 建造者模式 SqlSessionFactoryBuilder
      3. 代理模式 MapperProxy
      4. 装饰器模式 Cache
  4. 常用设计模式
    1. 工厂模式
      1. 简单工厂模式(静态工厂模式)
      2. 工厂方法模式
      3. 抽象工厂模式
      4. 为什么使用工厂模式?(优点)
    2. 装饰模式
      1. 适配器模式,装饰模式,代理模式区别
      2. Java IO中的装饰模式(InputStream-FilterInputStream)
    3. 适配器模式
      1. 类适配器
      2. 对象适配器
      3. 最小接口原则
      4. 缺省适配器
      5. 使用适配器模式克服观察者模式的缺点
    4. 代理模式
      1. 静态代理
    5. 策略模式
      1. 基础的策略模式实现
      2. 结合Spring容器实现策略模式(干掉if else代码)
    6. 模板方法模式
      1. JDK 中的 AQS 使用了模板方法模式
      2. Feign签名拦截器中使用模板方法模式
    7. 状态模式
      1. 策略模式和状态模式的区别
    8. 迭代器模式
    9. 观察者模式
      1. 观察者模式实例
      2. 推模式和拉模式
      3. Java内置观察者模式
    10. 单例模式
      1. 懒汉模式和饿汉模式
      2. 单例模式的线程安全性
      3. 实现单例模式的8中方法
        1. 1、饿汉式(静态常量)[可用]
        2. 2、饿汉式(静态代码块)[可用]
        3. 3、懒汉式(线程不安全)[不可用]
        4. 4、懒汉式(线程安全,同步方法)[不推荐用]
        5. 5、懒汉式(线程安全,同步代码块)[不可用]
        6. 6、双重检查[推荐用]
          1. 双重检查时的实例对象为什么必须是volatile的?
        7. 7、静态内部类[推荐用]
        8. 8、枚举(防止反序列化重建单例)[推荐用]
      4. 利用反射构造多个单例对象
      5. 反序列化对单例模式的破坏

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论