Java设计模式

设计模式三大分类

1570846061126

创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。

结构型模式:把类或对象结合在一起形成一个更大的结构。

行为型模式:类和对象如何交互,及划分责任和算法。

创建型模式

单例模式(Singleton Pattern)

确保类只有一个对象的模式,非常简单。直接上代码应该也看得懂吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {

// 这里可以让这个对象是final
private static Singleton instance;

private Singleton() {

}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

可以看到它的构造方法是私有的,所以只有通过暴露的getInstance方法才能访问,而在里面确保了只有实例没有初始化的时候才会去调用构造器来创建,一旦是有的话就不会创建。而且static修饰符确保了只会创建一个实例。上面的这个代码叫“懒汉式实现”。优点是真正要用到对象的时候才创建,所以资源利用率高。

但是这样真的是对的么?显然不是,当遇到多线程的时候就有问题,比如一个线程执行到了if (instance == null),并且进入了if里面去,此时被另外一个线程抢走了控制权,那么那个线程也会从if进去,显然就不对了;解决思路也很简单,加上synchronized就行了,这样能保证线程不会因为抢夺资源而出现问题。

但是这样真的好么?synchronized修饰本身是对资源一种很大的浪费,因为仅仅是在第一次才有可能会因为线程而出现问题,但是如果这么写的话,以后开销实在是太大了(一旦创建了实例,就不需要同步了,因为根本就进不到if里面去了),有两种解决办法:

  1. 直接在创建类的时候就把实例创建好了,当然前提是这个实例不是很吃资源,也叫饿汉式单例模式。非常简单:
1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {

private static Singleton instance = new Singleton();

private Singleton() {

}

public static Singleton getInstance() {
return instance;
}
}

优点是其线程安全,因为在类加载的时候进行了对象的创建,而在类加载的时候天然线程安全。既然线程安全了,不需要同步,所以效率很高。

  1. 双重检查锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton {

private volatile static Singleton instance;

private Singleton() {

}

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

}
return instance;
}
}

这里网上有资料说,因为编译器优化问题和JVM内部模型问题,可能会导致出错,所以反而不推荐使用这个。我自己查资料说的是JDK1.5之后就没有问题了。

这里把instance加上关键字volatile的最主要目的是为了防止在第13行发生指令重排,因为可能会先把引用返回给instance,然后再去初始化instance,如果真这样了,那么有可能别的线程拿到的instance已经不是null了,但是其实并没有初始化,进而发生错误。

这里没有用到volatile的可见性,因为synchronized保证了内部的所有变量都是可见的。

  1. 静态内部类,结合前几种的优势:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static class InnerSingleton {
private static final Singleton instance = new Singleton();
}

public static Singleton getInstance() {
return InnerSingleton.instance;
}

private Singleton() {

}
}

首先这个类只有在调用getInstance方法的时候,才会去加载静态内部类,而加载类的时候是线程安全的,由此问题得以解决。

  1. 枚举方式
1
2
3
4
5
6
7
8
9
10
11
12
13
public enum Singleton {
INSTANCE;

public void doSomething() {
// do something
}

public static void main(String[] args) {
Singleton s1 = Singleton.INSTANCE;
Singleton s2 = Singleton.INSTANCE;
System.out.println(s1 == s2);
}
}

因为枚举天然就是一个单例,所以你可以直接让对象成为枚举类的一个属性,就可以保证单例了。

一共有五种方法(包含例子中的,但是例子中需要加入synchronized关键字才可以),推荐使用静态内部类来实现单例。

之前我们也看到了,实现单例的时候是通过使用私有构造器方法来实现的,但是反射和反序列化可以无视private关键字,所以还需要进行一些处理来对付反射和反序列化(枚举类无惧反射和反序列化)。

反射的解决方法也比较简单,只需要在私有的构造器中,判断下不是空的话,就抛出异常。

反序列化的话,只需要重写readResolve方法,在其中返回对象即可。

工厂模式(Factory Pattern)

这个模式非常的简单,就是我一般创建对象会这么创建:

1
File file = new File();

工厂模式就是把这部分封装到了一个类的方法里面,然后通过这个类的方法就能够生成对象。

PS:这个模式并不是一个设计模式,只是因为用的太多了,所以这么称呼它。

简单工厂模式

首先这个设计模式并没有被归入到23种之内。

这是一开始最让人难以理解的设计模式,这是因为到目前为止,我们既是对象的创造者,又是对象的使用者,所以对于我们来说,创建对象和使用对象是天经地义的,而简单工厂的出现就是为了能够创造对象,这样我只需要知道如何使用工厂就可以创造出对象了。

1
2
3
4
5
6
7
// 一般调用
Dog d = new Dog();
d.bark();

//简单工厂
Dog d = DogFactory.createDog("哈士奇");
d.bark();

工厂方法模式(Factory Method Pattern)

这个设计模式并没有被归入到23种之内。

上面的工厂模式有个缺点,就是它其实就是让结构更清晰一点,其他可以说几乎没有什么用。而工厂方法模式可以让这个变得稍微有用一点,更加抽象一点。

简单来说就是设计一个工厂接口,然后让工厂们去实现这个接口,这样我要创建什么对象,只需要使用对应的工厂即可。优点是,工厂的添加可以完全不影响之前的代码。

抽象工厂模式(Abstract Factory Pattern)

抽象工厂模式,提供了一个接口来创建对象的家族,而不用指定具体类。说白了,抽象工厂就是生产工厂的工厂而已。

抽象工厂首先会定义一个接口,然后所有的工厂都必须实现这个接口,这个接口能够生产产品。而用户只会和这个抽象工厂(总公司)打交道。所以其实抽象工厂模式,用到了之前的工厂方法模式。它抽象了工厂的所有方法,然后客户利用它,来生成具体的工厂。

假设有个工厂它又卖鼠标又卖键盘,也就是 PC 厂商是个父类,有生产鼠标,生产键盘两个接口。

戴尔工厂,惠普工厂继承它,可以分别生产戴尔鼠标+戴尔键盘,和惠普鼠标+惠普键盘。(这里其实用到了工厂方法模式)

创建工厂时,由戴尔工厂创建。后续工厂.生产鼠标()则生产戴尔鼠标,工厂.生产键盘()则生产戴尔键盘。而如果我需要新增一个工厂,只需要继承这个新的抽象工厂类即可。

所以,抽象工厂模式和工厂方法模式的内在区别在于:抽象工厂模式的产品是”工厂“(披萨工厂,鼠标工厂,键盘工厂),而工厂方法模式的产品就是产品(鼠标,键盘,披萨)。

建造者模式(Builder Pattern)

这个模式之前用过,okhttp库中创建一个response对象,因为比较复杂,就不停通过增加组件来进行实现,其核心思路主要是return this来达成连缀。

原型模式(Prototype Pattern)

假设做一份卷子需要2小时,但是如果抄写,可能仅需要3分钟,这就是这个模式的由来:你new一个对象可能需要不小的开销,但是克隆一个,却轻轻松松简简单单。如果要克隆,需要实现Cloneable接口(空接口)和重写clone方法(这个方法不是Cloneable的,而是Object的),需要注意的一点,Java中所有的都是值传递,也就是Python中所谓的浅拷贝,所以如果你的对象中的属性也是一个对象,那么记得对于每个对象属性都进行clone。

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
29
public class Car implements Cloneable {
private String carName;
private int price;
private Date date;

public Car(String carName, int price, Date date) {
this.carName = carName;
this.price = price;
this.date = date;
}

@Override
protected Car clone() throws CloneNotSupportedException {
return (Car) super.clone();
// 如要实现所谓的“深拷贝”
// Object obj = super.clone();
// Car c = (Car) obj;
// c.date = (Date) this.date.clone();
}

public static void main(String[] args) throws CloneNotSupportedException {
Date date = new Date(123000000000L);
System.out.println(date);
Car c1 = new Car("car", 10, date);
Car c2 = c1.clone();
c1.date.setTime(1230000000001L);
System.out.println(c2.date);
}
}

当然也可以通过序列化和反序列化来进行操作,这样也能实现深复制。

结构型模式

装饰器模式(Decorator Pattern)

这个模式在学习java IO的肯定接触到过了,可能只是不知道这叫装饰器而已。如下所示:

1
InputStream inputStream = new BufferedInputStream(new FileInputStream("test.txt"));

为什么会有装饰器呢?因为原来的对象(FileInputStream)功能不足(没有缓存功能),所以需要别的对象来加强它原来的功能。

  1. 创建接口:
1
2
3
public interface Shape {
void draw();
}
  1. 用两个类来实现它:
1
2
3
4
5
6
7
public class Rectangle implements Shape {

@Override
public void draw() {
System.out.println("Shape: Rectangle");
}
}
1
2
3
4
5
6
7
public class Circle implements Shape {

@Override
public void draw() {
System.out.println("Shape: Circle");
}
}

这两个类的功能非常贫乏,就是输出他们的形状。

现在轮到装饰器上场了:

  1. 创建装饰器:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ShapeDecorator implements Shape {
Shape shape;

public ShapeDecorator(Shape shape) {
this.shape = shape;
}

@Override
public void draw() {
shape.draw();
System.out.println("this function is added by decorator ");
}
}
  1. 使用装饰器:
1
2
3
4
5
6
7
8
9
public class Demo {
public static void main(String[] args) {
Shape cirtle = new Circle();
cirtle.draw();

cirtle = new ShapeDecorator(cirtle);
cirtle.draw();
}
}

装饰器的核心在于:实现同一个接口,然后创建一个接口的成员变量,并且在构造函数中传入并初始化(这一步确保能够用类的对象来作为参数),然后重写需要增强的功能。

适配器模式(Adapter Pattern)

在java.io中的转换流中我们就用到过这一模式,InputStreamReader和OutputStreamWriter就是最典型的例子。这些类其实内部持有了一个需要转化的对象,然后再内部对其进行处理,之后外部只需要调用即可。

代理模式(Proxy Pattern)

有一个真实来完成任务的对象,还有一个该对象的代理,这两个都实现某一个接口,然后客户只需要跟这个接口打交道就行。静态代理非常简单,只需要持有一个真实对象的引用,然后在它对应的方法里使用即可。动态代理我看的云里雾里,打算先放下,之后遇到了回来填坑吧。

桥接模式(Bridge Pattern)

平时我也写过一些小工具,有的时候会用到A继承B,B继承C,这样开枝散叶之后,最后如果要新增一个大类,会非常非常麻烦。而且本身开枝散叶这件事写代码量也是非常巨大,而且枯燥无聊。

解决办法就是使用降维打击,对于多个变化维度,它们的排列组合出来的东西会非常多,所以我们可以抽象出维度,然后在任意一个类中,加入其它维度的信息,这样就可以实现降维打击。

组合模式(Composite Pattern)

这个模式很简单,就是有两种类,它们都继承了某一个接口,然后这两种类,其中有一个是叶子节点,另外一个类是容器节点,容器节点可以存放叶子节点。这样我不论怎么调用这两种类,都只需要调用接口的方法即可,相当于这两种类虽然是不同的,但是在使用者看来没有差别。

外观模式(Facade Pattern)

在tomcat中,用来隐藏很复杂的Request和Response被使用。在现实中的例子就是,比如看病需要挂号、付钱、取药等一系列复杂的操作,但是现在如果有一个管家能帮你完成这一切,这个管家就是外观模式的应用。

享元模式(Flyweight Pattern)

java中某些对象的开销比较大,可以利用这种设计模式来有效重复利用,核心思想就是通过HashMap这种数据结构。

行为型模式

策略模式(Strategy Pattern)

一句话概括:策略模式就是定义一系列算法,然后把它们封装起来,并且使它们可以相互替换。

为什么要有这个模式?因为不想if…else…泛滥。因为说到了算法之间的替换,所以这些算法只需要实现同一个接口,然后就可以替换了。

比如现在我希望能够实现加减乘除,首先映入脑海的就是类似下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public int doMath(String type, int a, int b) {
if (type.equals("+")) {
return a + b;
} else if (type.equals("-")) {
return a - b;
} else if (type.equals("*")) {
return a * b;
} else if (type.equals("/")) {
return a / b;
} else {
return 0;
}
}

当然如果以后有更多的操作符,那就需要更多的if….else….,会不好维护。所以就可以用策略模式来代替。

首先对所有的算法抽象出它们所共有的:

1
2
3
public interface MyMath {
public int doMath();
}

然后对这些算法分别实现。

1
2
3
4
5
6
public class MyAdd implements MyMath {
@Override
public int doMath(int a, int b) {
return a + b;
}
}
1
2
3
4
5
6
public class MySubstract implements MyMath {
@Override
public int doMath(int a, int b) {
return a - b;
}
}

最重要的一步,加入一个“中间层”,让这个“中间层”来”选择算法”进行操作。

1
2
3
4
5
6
7
8
9
10
11
public class Context {
MyMath myMath;

public Context(MyMath myMath) {
this.myMath = myMath;
}

public int doMath(int a, int b) {
return myMath.doMath(a, b);
}
}

最后就可以在其他类中方便的使用了。

1
2
3
4
5
6
7
8
9
public class Demo {
public static void main(String[] args) {
Context context = new Context(new MyAdd());
System.out.println(context.doMath(5, 6));

context = new Context(new MySubstract());
System.out.println(context.doMath(5, 6));
}
}

这么做的优势,就是如果我有新的操作方式,我就可以直接写一个类来继承MyMath,其他完全都不需要动。

观察者模式(Observer Pattern)

有点类似我们现实中订阅报纸,我向报社付了钱(订阅),然后报社就会每天给我送一份报纸。在软件中,就相当于是有个组件对于另外一个组件感兴趣,那么就可以用这个模式,让组件发生变化的时候,发送变化给“订阅”了变化的组件。说白了,就是报社有一份订阅者的名单(ArrayList),然后自己变化了(自己当然知道自己发生了变化)就对这个表里的所有人发送消息(调用他们的方法)。

  1. 创建一个报社的接口:
1
2
3
4
5
6
7
8
9
public interface Newspaper {
void addSubscriber(Subscriber subscriber);

void removeSubscriber(Subscriber subscriber);

void notifySubscribers();

void setNewspaper(int newspaper);
}
  1. 创建订阅者接口(订报纸的人):
1
2
3
public interface Subscriber {
void getNewspaper(int newspaper);
}
  1. 实现一个具体的报社:
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
29
public class BeijingNewspaper implements Newspaper {

private ArrayList<Subscriber> subscribers = new ArrayList<>();
private int newpaper = 0;

@Override
public void addSubscriber(Subscriber subscriber) {
subscribers.add(subscriber);
}

@Override
public void removeSubscriber(Subscriber subscriber) {
//这里只是为了简单说明问题,并没有做检查
subscribers.remove(subscriber);
}

@Override
public void notifySubscribers() {
for (Subscriber s : subscribers) {
s.getNewspaper(5);
}
}

@Override
public void setNewspaper(int newspaper) {
this.newpaper = newpaper;
notifySubscribers();
}
}

这里类很简单,就是用一个ArrayList来记录订阅者,然后每当有新的报纸(用整数来模拟),就会通知所有的订阅者。

  1. 实现一个具体的订阅者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MenSubscriber implements Subscriber {

Newspaper newspaper;

public MenSubscriber(Newspaper newspaper) {
this.newspaper = newspaper;
newspaper.addSubscriber(this);
}

@Override
public void getNewspaper(int newspaper) {
System.out.println("get newspaper = " + newspaper);
}
}
  1. 最后进行测试
1
2
3
4
5
6
7
public class Demo {
public static void main(String[] args) {
Newspaper newspaper = new BeijingNewspaper();
new MenSubscriber(newspaper);
newspaper.setNewspaper(5);
}
}

我觉得这个模式最主要的部分是理解其实是订阅者自己有一个订阅表,然后把自己写进了那个订阅表,这样报社才会把报纸发给它。