Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

单例模式

  • 使用场景:有些情况下,我们只需要一个实例,比如说:点击按钮之后的弹框,应该是点击多次也只弹一次,而不是点击一次弹一次。这种情况就需要用到单例模式,从功能上讲,单例模式用于那些只需要一个实例的场景;从资源上讲,单例模式可以避免对象频繁的创建和销毁。
  • 如何实现:构造方法私有,通过getInstance()方法来获得实例。
  • 实现代码
  1. 懒汉式,线程不安全

    所谓懒汉式,就是说并没有在定义intance的时候就给它初始化,因为懒,只有到第一次用到这个实例的时候才去初始化。懒汉模式是一种lazy-loading,只有到用的时候才会创建,从而也就会有个问题,第一次使用的时候创建可能会比较耗时。如果我们知道这个对象肯定是要被用到的,可以用饿汉模式,下面会说到。

    但是很显然在多线程环境下是会出问题的,如果两个线程同时执行到了if(instance==null),然后发现都成立,就会new出两个实例来。

1
2
3
4
5
6
7
8
9
10
public class SingleObject{
private static SingleObject instance ;
private SingleObject(){};
private static SingleObject getInstance(){
if(instance == null){
instance = new SingleObject() ;
}
return instance ;
}
}
  1. 懒汉式,线程安全

这种方法在第一次调用时才初始化,避免浪费内存。但是需要加锁才能保证单例,会影响效率。因为只有new的时候,也就是第一次初始化的时候才需要并发控制,其他情况下是不需要并发控制的,但是给这个getInstance()方法加上synchronized关键字,会导致每次需要获得单例的时候都会被加锁。

1
2
3
4
5
6
7
8
9
10
public class SingleObject{
private static SingleObject instance ;
private SingleObject(){};
private static synchronized SingleObject getInstance(){
if(instance == null){
instance = new SingleObject() ;
}
return instance ;
}
}

可以看出这种方式一个问题就是,锁的粒度太大了,其实只需要在new对象的时候加锁就可以了,这就引出了下面双重校验锁。

  1. 饿汉式,线程安全

所谓饿汉,就是说在类加载的时候就初始化。它这里其实是利用了类加载机制,类加载的时候是会用synchronized关键字给加锁的。未实现lazy-loading.

1
2
3
4
5
6
7
public classs SingleObject{
private static SingleObject instance = new SingleObject() ;
private SingleObject(){};
private static SingleObject getInstance(){
return instance ;
}
}
  1. 饿汉变种
1
2
3
4
5
6
7
8
9
public class Singleton{
private static class SingletonHolder{
private static final Singleton instance = new Singleton() ;
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHolder.instance ;
}
}

同样是利用类加载机制,同样是饿汉,但是它实现了lazy-loading。类加载的时候Singleton被装载,但是instance不一定被初始化,只有调用getIntance()方法的时候,才会显式装载SingletonHolder类,从而实例化instance

  1. 双重校验锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static SingleObject{
private volatile static SingleObject instance ;
private SingleObject(){};
private static SingleObject getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance==null){
instance = new SingleObject() ;
}
}
}
return instance ;
}
}

double check在代码中已经体现的很明显了,这里主要解释一下volatile关键字。在jvm的文章中,我们已经提到volatile关键字可以保证可见性、禁止指令重排序。它用在这里的的主要作用也是为了保证禁止指令重排序,在初始化一个实例的时候,要经过一下几个步骤

(1)申请内存空间

(2)初始化默认值(注意不是构造方法的初始化)

(3)执行构造方法

(4)连接引用和实例

其中步骤(3)对应new SingleObject(),步骤四对应instance=new SingleObject(),这四个步骤可能会进行指令重排序,变成(1)(2)(4)(3),这种情况下,上述代码执行完后可能会return一个还没有初始化完的实例,另一个线程获取时,就会获取到一个未初始化完的对象。而使用了volatile关键字后,可以禁止指令重排序,那么执行的顺序就是(1)(2)(3)(4),就不会存在上述问题。

  1. 静态内部类
1
2
3
4
5
6
7
8
9
public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
  1. 枚举
  2. CAS

Iterator

Iterator本身是个Interface,被很多集合类所实现,我们也可以自己实现。

使用Iterator的一个好处就是我们可以不用去关注序列本身具体的结构,只用操作Iterator就可以实现序列的遍历。

如何实现

在集合类内部实现一个Iterator接口,在这个实现的接口里面对集合类进行访问。而外部类想要访问集合中的元素时,只需要给它返回一个Iterator对象,然后通过Iterator的next()和hasNext()方法来访问集合元素。

这样做的一个好处:可以不对外部类暴露集合内部情况。

这里是代码部分

其实感觉设计模式的核心就是实现功能的解耦,让每个部分各司其职,而不是一锅大杂烩。

Visitor

当对象结构对应的类很少改变,但经常需要在此结构上定义很多不同且不相关的操作,为了不让这些操作污染这些对象的类,我们将这些操作封装在另一个类中,而在被访问的类中只提供一个接待访问者的接口accept(Visitor),在Visitor()中传入被访问类的引用供访问。

类似于有人来你家做客,你只负责给他开个门(被访问的类提供一个accept()接口让visitor进来),他来到了你家,对你家的结构很清楚了(visit方法有一个被访问对象作为参数),至于客人具体要做什么,完全自便(具体的visit()方法的内容由Visitor定)。

这种设计的好处:

  • 很好的解耦了访问者和被访问者
  • 给被访问者提供了很大的自主操作的空间。

ComputerPart 接口

1
2
3
4
5
package Visitor;

public interface ComputerPart {
void accept(ComputerPartVisitor visitor) ;
}

ComputerPart实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package Visitor;

public class Keyboard implements ComputerPart {
@Override
public void accept(ComputerPartVisitor visitor) {
visitor.visit(this);
}
}

package Visitor;

public class Mouse implements ComputerPart {

@Override
public void accept(ComputerPartVisitor visitor) {
visitor.visit(this);
}
}

package Visitor;

public class Monitor implements ComputerPart {
@Override
public void accept(ComputerPartVisitor visitor) {
visitor.visit(this);
}
}

Visitor接口

1
2
3
4
5
6
package Visitor;

public interface ComputerPartVisitor {
void visit(ComputerPart computerPart) ;
}

Visitor接口实现类

1
2
3
4
5
6
7
8
9
10
11
package Visitor;

public class ComputerPartVisitorImp implements ComputerPartVisitor {


@Override
public void visit(ComputerPart computerPart) {
System.out.println("visit "+computerPart.getClass());
}
}

当我们需要访问更多对象时,可以在Visitor中多重载几个visitor函数即可。

Composite

也叫做部分-整体模式,把对象组合成树形结构,将个体对象(叶子)和组合对象(树枝)统一对待。

它是一种嵌套的关系,就像目录树一样,一个文件夹套一个文件夹,直到最后的叶子节点——文件;或者像一句话一样,从句子到单词再到最后的字母;还有HTML标签,等等。

当我们想忽略组合对象与个体对象的不同,统一使用组合结构中的所有对象时,可以考虑这种设计模式。

Implementing the composite pattern lets clients treat individual objects and compositions uniformly.

如何解决:树枝和叶子实现统一接口,树枝内部组合该接口。

关键代码:树枝内部组合该接口,并且含有内部属性 List,里面放 Component

结构

一般来讲,组合模式的Composite里面会有一个Arraylist,add方法既可以向其中添加Leaf,也可以添加composite,这里就体现了个体和组合对象的统一对待。

这里是相关代码实现,代码中除了add方法能体现该思想外,printList方法也能体现,对于File的print,只是简单的打印,对于Directory的print则是通过迭代器,去打印其每一个item,这里其实是个递归调用,如果item是file,那么就调用了File的·printList方法,如果item是directory,则递归调用。

Adapter

适配器模式,顾名思义,就像电源适配器一样,你需要对已有的东西进行一定的改装,使他适配于新的需求。

比如说我原本就有一个现成的类,现在有一个新项目,也能用到这个类,但是需要稍作修改(旧类的接口不符合新系统的需要)。

再具体一点,新类A想用旧类B中的一些方法,但是得需要修改一下,因为这些方法不能完全满足现在的需求。有这么几种想法:
  • 直接修改旧类B (这个方法很暴力)

  • 新类A继承旧类B (Java中类只能单继承,而且这两个类逻辑上也不是继承关系,如果继承并覆盖的话还不如重新实现)

  • 适配器模式 (通过一个中间件把类A的东西转化成类B的)

适配器模式就像生活中的电源适配器一样,能够连接两头并且进行转换。举个栗子,比如说有个Chinese类,他能speak和write,但是都是中文,后来,需求改变,又需要再写一个American,他也能speak和write,但是是英文。那么问题来了,如果重新写一个类的话,其实很多代码都是和Chinese相同的,这时候就需要适配器模式了。适配器模式有两种实现方法
  • 继承已有类,实现目标接口
  • 依赖已有类(将已有类作为自己的元素),实现目标接口

这里实例一个依赖的实现

Chinese类

1
2
3
4
5
6
7
8
9
10
Public class Chinese{
public String write(){
System.out.println("我正在写汉字");
return "我正在写汉字" ;
}
public String speak(){
System.out.println("我正在说汉语");
return "我正在说汉语" ;
}
}

American接口

1
2
3
4
Public interface American{
public void writeEnglish();
public void speakEnglish();
}

适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Public class TranslatorAdapter implement American{
Chinese chi ;

public TranslatorAdapter(Chinese chi){
this.chi = chi ;
}

@Override
public void writeEnglish(){
System.out.println(translate(chi.write()));
}

public void speakEnglish(){
System.out.println(translate(chi.speak())) ;
}

public String tranlate(String sentence){
if(sentence.equal("我正在写汉字"))
return "I am speak English" ;
return "I am writing English" ;
}

}

测试类

1
2
3
4
5
Pulic class Test{
public static void main(String[] args){
TranslatorAdapter adapter = new TranslatorAdapter(new Chinese())
}
}

Template Method Pattern

Template Method模式用到了抽象类,用一句话总结它的特点就是:父类中定义处理流程的框架,子类中实现具体的处理。模板顾名思义,就是提供了一个模板,最后的模具和它的形状相同(子类继承父类),但是但是不同的模具用的材料可能不同(继承自抽象父类的子类的实现方法不尽相同)

This is a picture without description

这里是一个简单的demo,需要注意两个问题

  1. 模板使用final关键字修饰,不可改变
  2. 具体方法延迟到子类中去实现

Simple Factory Pattern

模式动机

假设这样一种场景:有一个父类按钮Button, 这个父类按钮底下继承了很多不同形状的子类按钮,圆形的,正方形的,三角形的。。。现在客户端程序想要用这些按钮,一种方法是你说明要哪种按钮,然后在客户端中new出来,但是显然这样耦合度比较高,因为在客户端中我们想用就完事了,而不想多余的先判断,再new.  另一种可行的办法是,我们再重新建一个类,叫做工厂类,当客户端中想要什么类型的按钮时,就跟工厂类讲,比如客户端需要一个圆形按钮,它不需要知道这个按钮的类名叫什么,它只需要告诉工厂类,我需要一个圆形按钮,工厂类造好给它就可以了。

这就是简单工厂模式,就是说有一个专门的工厂类用来生产一堆大差不差的子类,外部只需要告诉它要什么类,它生产好了再交给外部。

UML

再偷一张菜鸟教程的图嘿嘿嘿

This is a picture without description

这个很容易理解,就不写代码了

要说简单工厂模式和策略模式的区别,工厂模式生产的是类,而策略模式注重的是策略,也就是方法,

工厂类根据不同的需求返回不同的类,策略模式根据外部传入的不同的类,给出(执行)不同的方法(策略)

工厂方法模式

上述简单工厂模式中,有一个工厂类,客户端告诉工厂需求,它根据需求创建好对应的类并返回。这就存在一个问题,你需要在工厂类中判断当前是哪种需求,这就意味着以后要扩展时,你肯定要在工厂类中做修改,增加新的需求判断条件,这就违背了开闭原则,因为,我们提出了工厂方法模式。

简单工厂模式只有一个工厂类,根据不同需求产生不同的product, 工厂方法模式有多个工厂类,不同的需求对应不同的工厂类,这些工厂类实现了共同的接口。当日后要扩展时,只需要添加product类,添加对应工厂类就可以了。这种修改同时意味着我们将需求的判断交给了客户端,也就是说,客户端需要告诉工厂类它想要什么具体产品,而不是像简单工厂模式那样这个判断是在工厂类中进行的。

抽象工厂模式

工厂方法模式中,一个工厂生产一种特定的产品,工厂方法也具有唯一性。但是也有一种情况,一个工厂要生产多种产品,如一个海尔公司要生产海尔电视,海尔冰箱,海尔洗衣机;又如一个SqlServer工厂要生产user表,部门表等,一个Access工厂也要生产user表,部门表等。对于这种一个工厂要生产多种不同产品的情况,我们采用抽象工厂模式。

This is a picture without description

Decorator

​ 装饰器模式,在不违背开闭原则的基础上,给一个类动态的添加功能。也就是说在不修改这个类且不影响其他类的基础上,给这个类添加功能。

​ 对于一个设计模式,理解它的原理是很简单的,但是要理解它为什么是这样的,应该在哪些场景中使用它,却是不容易的。下面我将试着从一个开发者的角度去思考这个问题。

​ 假如有一个类 A,现在需求升级,要给A添加新的功能,该怎么做?

  1. 直接修改A,简单粗暴,但同时也有很多隐患。
  2. 写一个子类继承A,在子类中添加新的功能。可以是可以,不够优雅,有新功能的A和原来的A原则上讲并没有继承关系,只是我们为了实现需求给强行继承了。
  3. 使用装饰器模式。

装饰器模式,听名字就知道,它装饰了原来的类。就像明月装饰了你的窗户,你装饰了别人的梦。抛去这些花里胡哨的名词,我们来看他的本质。

既然是装饰,其实可以理解为一种“包裹”,我在原来的类的基础上再包裹一些东西。那么问题来了,怎么包裹呢?首先,为了让外人看不出我的装饰器类和原来的A类的差别,我让装饰器类和A类继承自同一父类;

然后,为了实现包裹,我把原来的A类作为装饰器类的一个构造函数参数传给装饰器类,这样装饰器类既能够使用A类,又能够在A类的基础上进行一些操作了。(这个包裹说的专业点就叫做聚合)

以上。

This is a picture without description

​ 从UML图可以看出,其实Decorator和ConcretComponent继承自同一父类,Decorotor它聚会了Component类,然后在复写的operation方法中对聚合而来的component类进行操作,这就是所谓的装饰。

​ 其实装饰器模式的本质就是,在一个装饰器类中引入了原本要修改的类,然后对这个类进行装饰。要被修改的类的代码没有发生改变,只是它自己被放在另一个类中被操作了一波。

​ 它的核心思想:1. 继承自相同的父类(这样就可以复写要修改的类中的方法) 2. 聚合了要修改的类(这样就可以在原来类的基础上进行一定的装饰,而这个装饰代码是在原来的类之外的。)

Proxy Pattern

代理模式,故名思意。加入我们想访问对象A,代理模式就是在我们和对象A之间建立一个中间层,也就是代理,从而让代理替我们去访问这个对象。

它的核心思想就是让代理和被访问对象实现同一接口,然后在代理中调用被访问对象中的方法来实现访问。

原型模式

Java中原型模式通过实现`Cloneable`接口来实现。所谓原型模式就是说,这个接口有个clone方法,当我们需要这个对象时,通过clone方法来获得对象,而不是通过类的构造函数来构造类,这两者的代价是很不同的。

所使用的场景:当直接创建对象的代价比较大时,则采用这种模式。

​ 所以这里就会存在一个浅复制和深复制的问题。

外观模式

假如你接手祖传代码后,使用起来肯定不容易,这时候你可以在祖传代码之上再加一个中间层,外部代码通过访问中间层来实现对祖传代码的访问,这样就避免了外部代码直接去访问祖传代码。

外观模式隐藏系统的复杂性。它向现有的系统提供一个接口,来隐藏系统的复杂性。客户端不需要知道系统内部的复杂联系,整个系统只需要提供一个接待员即可。

关键代码:在客户端和复杂系统之间再加一层,这一层将调用顺序、依赖关系等处理好。

建造者模式

将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。(感觉有点像模板模式)

建造者模式更加关注零件装配的顺序。就是说组件不变,而这些组件之间的组合会发生变化。

如何实现:首先有个builder类,用来有构建各个组件的方法,其次有个Director类,用来描述各个组件之间的组合关系。

This is a picture without description

观察者模式

一个对象的状态发生变化,给所有依赖它的对象发送通知,进行更新。

This is a picture without description

策略模式

设计模式的终极目的在于解耦。

加入完成一个任务,有多种方法,我们希望根据不同的环境或者不同的条件这多种方法可以随时替换。

比如说现在有个计算器,有时候我们要它做加法,有时候又要它做减法,那么怎么实现呢?

最直观的方法就是硬编码了,直接把这些加减乘除写成一个计算器Calculator的方法,如果这样的话后期修改也就比较麻烦。

策略模式的思想就在于把这些不同的算法封装成不同的策略类,然后聚合到Calculator类中,然后再给Calculator类一个set方法,以此实现可插拔。

再盗一张菜鸟教程的图。

chrome_yj93hjB8tG.png

评论