Designpatterns | Eloise's Paradise
0%

Designpatterns

本博客对Java中常见的设计模式进行简要的介绍汇总.

单例模式

引子

很多场景下我们只需要一个该类型的对象, 比如: 线程池(ThreadPool), 缓存(Cache), 对话框(Dialog), 处理偏好设置和注册表(Registry)的对象, 日志对象, 充当打印机/显卡等设备的驱动程序的对象等等. 事实上, 这类对象也只能 有一个实例, 如果多了就会导致许多问题. 比如: 程序异常, 资源使用过量, 结果不一致等. 那么这种只需要一个实例对象的编程模式在软件工程上被抽象为单例模式.

定义

单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。

基本思路

单例模式的基本思路就是在类中想构造函数私有化, 然后提供一个public的获取实例对象的方法让外部获取, 并且确保外部多次获取返回的都是同一个对象.

实现方式

一. 饿汉式

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.lee.tank.designPatterns.Singleton;

public class Mgr01 {
private Mgr01() {}; //step 1. 构造函数私有化, 保证外部(其他类无法通过new的方式创建对象)
private static final Mgr01 INSTANCE = new Mgr01();//step 2. 内部创建静态实例, static保证了类一加载就实例化, , final保证了实例的唯一性
public static Mgr01 getInstance() { //step 3. 返回step2创建的实例, 并将该方法对外开放.
return INSTANCE;
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2); //true
}
}

特点

类加载到内存后,就实例化一个单例,JVM保证线程安全
简单实用,推荐使用!
唯一缺点:不管用到与否,类装载时就完成实例化
Class.forName(“”)
(话说你不用的,你装载它干啥)

二. 饿汉式变体

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.mashibing.dp.singleton;

/**
* 跟01是一个意思
*/
public class Mgr02 {
private static final Mgr02 INSTANCE;
static {
INSTANCE = new Mgr02(); //通过静态代码块的方式初始化实例INSTANCE, 本质和饿汉式一样
}

private Mgr02() {};

public static Mgr02 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
Mgr02 m1 = Mgr02.getInstance();
Mgr02 m2 = Mgr02.getInstance();
System.out.println(m1 == m2);
}
}

三. 懒汉式

代码

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
30
31
32
package com.lee.tank.designPatterns.Singleton;

/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
*/
public class Mgr03 {
private static Mgr03 INSTANCE;

private Mgr03() {
}

public static Mgr03 getInstance() {
if (INSTANCE == null) { // 多线程调用该方法时, 可能存在某个线程判断完INSTANCE == null之后, 返回实例之前被其他线程抢占资源,再判断
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03(); // 最终造成这里会执行两次实例化
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->
System.out.println(Mgr03.getInstance().hashCode())
).start();
}
}
}

特点

虽然达到了按需初始化的目的,但却带来线程不安全的问题, 如下图:

单例_懒汉式

测试结果

单例_懒汉式_结果

四. 懒汉式变体(线程安全但低效)

懒汉式的改进, 满足线程安全

代码

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
30
31
32
33
34
35
36
37
38
package com.mashibing.dp.singleton;

/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Mgr04 {
private static Mgr04 INSTANCE;

private Mgr04() {
}

public static synchronized Mgr04 getInstance() { //Mgr03版本的优化, 能够做到线程安全
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}

public void m() {
System.out.println("m");
}

public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode());
}).start();
}
}
}

特点

虽然懒汉式达到了按需初始化的目的,但却带来线程不安全的问题, 该版本就是通过将懒汉式的getInstance方法同步化(synchronized)来解决线程安全的问题的.

但这同时也带来效率下降

五. 懒汉式变体(高效但线程不安全)

代码

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
30
31
32
33
34
35
36
package com.mashibing.dp.singleton;


public class Mgr05 {
private static Mgr05 INSTANCE;

private Mgr05() {
}

public static Mgr05 getInstance() {
if (INSTANCE == null) { // 此处还是会存在线程不安全隐患
//妄图通过减小同步代码块的方式提高效率,然后不可行
synchronized (Mgr05.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}

public void m() {
System.out.println("m");
}

public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr05.getInstance().hashCode());
}).start();
}
}
}

特点

为了尝试优化Mgr04的低效特点, 尝试将同步代码范围缩小以提上效率. 所以将同步函数变为了同步代码块. 但是, 此时 if(INSTANCE==null)的判断不属于同步块之后会造成线程安全隐患. 所以依旧不可取.

六. 懒汉式变体(高效&安全)

代码

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
30
31
package com.lee.tank.designPatterns.Singleton;

public class Mgr06 {
private Mgr06() {
}
private static volatile Mgr06 INSTANCE; //注意要加上volatile, 涉及到JIT指令重排的问题

public static Mgr06 getInstance() { //双重判断
if (INSTANCE == null) { // 第一次判断是否null主要能够起到提升效率的作用, 如果非空就直接返回, 否则才继续内层逻辑
synchronized (Mgr06.class) {
if (INSTANCE == null) { // 这层的判断
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;

}

public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}

特点

V7 静态内部类方式

代码

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
package com.mashibing.dp.singleton;

/**
* 静态内部类方式
* JVM保证单例
* 加载外部类时不会加载内部类,这样可以实现懒加载
*/
public class Mgr07 {

private Mgr07() {
}

private static class Mgr07Holder {
private final static Mgr07 INSTANCE = new Mgr07();
}

public static Mgr07 getInstance() {
return Mgr07Holder.INSTANCE;
}

public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr07.getInstance().hashCode());
}).start();
}
}

}

特点

JVM加载内部类时不会加载内部类, 所以 当Mgr07被加载时, Mgr07Holder 就不会被加载. 所以也就实现了懒加载. 另外, JVM加载某个类时只加载一次, 所以只有当外部用户调用Mgr07的getInstance方法时才会用到Mgr07Holder类, 才回去加载Mgr07Holder, 但是有只加载一次, 所以也*保证了线程的安全性. *

V8

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.lee.tank.designPatterns.Singleton;

/**
* 不仅可以解决线程同步,还可以防止反序列化。
*/
public enum Mgr08 { //枚举类型反编译是一个抽象类, 所以从这点来看外部是无法通过new来获取其实例对象的.

INSTANCE;

public void m() {}

public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr08.INSTANCE.hashCode());
}).start();
}
}

}

特点

不仅可以解决线程同步,还可以防止反序列化.

单例总结

1
2
3
4
5
6
/*
* 1. 实际工作中用的最多的还是饿汉式 Mgr01
* 2. 语法最完美的是枚举V8版本,写法简单的同时满足线程同步和反序列化后对象和反序列化之前对象时同一个的问题. (JAVA创始人在 effective Java一书中提出的)
* 3. 3-->6 逐步优化的过程(即3,4,5为什么不可取的原因)是面试中最常问的.
* 4. 1, 6, 7, 8 做到默写
*/

Strategy策略

Scenario

比如说有这样一个场景:

首先需要对int 类型的一个数组int[] arr进行排序, 比较自然的想法就是定义一个工具类Sorter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.lee.tank.designPatterns.Strategy;

public class Sorter {

public void sort(int[] arr) {
for(int i=0; i<arr.length - 1; i++) {
int minPos = i;

for(int j=i+1; j<arr.length; j++) {
minPos = arr[j]<arr[minPos] ? j : minPos;
}
swap(arr, i, minPos);
}
}

void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

然后在需要对数组进行排序的时候调用sort(int[] arr)方法即可. 如下:

1
2
3
4
5
6
7
8
9
10
11
12
package com.lee.tank.designPatterns.Strategy;

import java.util.Arrays;

public class Strategy {
public static void main(String[] args) {
int[] arr={3,5,2,5,1,2,7,9,0,10};
Sorter sorter = new Sorter();
sorter.sort(arr); // 调用排序
System.out.println(Arrays.toString(arr));
}
}

运行上面的代码确实能达到排序的目的

策略_1初始排序

Extension

那么 问题来了, 如果再想对double类型数组排序, 该怎么办呢? 就只能重写Sort类或者重载里面的sort方法:

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
30
31
32
33
34
35
36
37
38
package com.lee.tank.designPatterns.Strategy;

public class Sorter {

public void sort(int[] arr) {
for(int i=0; i<arr.length - 1; i++) {
int minPos = i;

for(int j=i+1; j<arr.length; j++) {
minPos = arr[j]<arr[minPos] ? j : minPos;
}
swap(arr, i, minPos);
}
}

public void sort(double[] arr) { // 新增可以对double[]类型排序的方法
for(int i=0; i<arr.length - 1; i++) {
int minPos = i;

for(int j=i+1; j<arr.length; j++) {
minPos = arr[j]<arr[minPos] ? j : minPos;
}
swap(arr, i, minPos);
}
}

void swap(double[] arr, int i, int j) { //新增
double temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

然后需要对double进行排序时进行调用sort(double[] arr_d)即可, 如下:

策略_1初始排序

Adapter

那么如果是对猫Cat, 狗Dog, 人Person进行排序, 是不是也要进行相应类型的重载sort(Type[] t)才能达到目的呢?

所以, 就有了这样一种想法, 能不能重新定义sort方法里面的比较大小, 不是直接按照大小, 而是可以根据各自类型的特有属性去个性化比较, 比如比较猫时按照年龄比较, 比较狗时按照体重比价, 那就就有了下面的代码

​ step 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
30
31
32
package com.lee.tank.designPatterns.Strategy;

public class Cat {
String name;
int age;


public Cat(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

int compareTo(Cat c){ // 对猫进行比较"大小"的方法
if(this.age < c.age) {
return -1;
}else if(this.age > c.age) {
return 1;
}else {
return 0;
}

}

}

然后在sort(Cat[] c)方法比较方式进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.lee.tank.designPatterns.Strategy;

public class Sorter {

public void sort(Cat[] arr) {
for(int i=0; i<arr.length - 1; i++) {
int minPos = i;

for(int j=i+1; j<arr.length; j++) {
// 以前是直接 arr[i]<arr[j] 比较, 现在是调用Cat的compareTo方法比较
minPos = arr[j].compareTo(arr[minPos])==-1 ? j : minPos;
}
swap(arr, i, minPos);
}
}

private void swap(Cat[] arr, int i, int j) {
Cat temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

Almighty-Adapter

但是似乎又忘了, 一开始, 我们是想让sort方法适应所有类型, 而不是某一种类型. 所以对sort(Cat[] c)方法进行类型适配是必须的.

因为是适配所有类型, 所以很自然的想到适配sort(Object[] obj)的样子, 但是obj并没有compareTo方法. 所以我们的想法是, 自定义一个接口不妨叫Comparable, 里面有一个compareTo方法. 然后有排序需求的实体类必须implements Comparable 实现该接口, 实现其compareTo()方法, sort方法重写成sort(Comparable[] c)的形式, 即可排序.

所以总体需要如下3个步骤:

step 1. 自定义接口

1
2
3
4
5
package com.lee.tank.designPatterns.Strategy;

public interface Comparable {
int compareTo(Object[] obj);
}

step 2. 实体类实现接口

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
30
package com.lee.tank.designPatterns.Strategy;

public class Dog implements Comparable{
int height ;
int weight ;
public Dog(int height, int weight) {
this.height = height;
this.weight = weight;
}

@Override
public String toString() {
return "Dog{" +
"height=" + height +
", weight=" + weight +
'}';
}

@Override
public int compareTo(Object obj) {
Dog d=(Dog) obj;
if(this.weight<d.weight){
return -1;
}else if(this.weight>d.weight){
return 1;
}else{
return 0;
}
}
}

step 3 重写sort方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.lee.tank.designPatterns.Strategy;

public class Sorter{

public void sort(Comparable[] arr) { //注意此处是Comparable[] 类型, 因为我们在自定义的接口里面定义了compareTo方法.
for(int i=0; i<arr.length - 1; i++) {
int minPos = i;

for(int j=i+1; j<arr.length; j++) {
minPos = arr[j].compareTo(arr[minPos])==-1 ? j : minPos;
}
swap(arr, i, minPos);
}
}

private void swap(Object[] arr, int i, int j) {
Object temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

step 4 调用验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.lee.tank.designPatterns.Strategy;

import java.util.Arrays;

public class Strategy {
public static void main(String[] args) {
Sorter sorter = new Sorter();
Cat[] cats={new Cat("Alex",3), new Cat("Brox",2),new Cat("Joshua",4), new Cat("Ted",1)};
sorter.sort(cats);
System.out.println(Arrays.toString(cats));

Dog[] dogs={new Dog(5,10),new Dog(7,8),new Dog(6,9)};
sorter.sort(dogs);
System.out.println(Arrays.toString(dogs));

}
}

策略_2适配Object排序

Generic

但是这种方式有一个弊端: 因为对Object做的适配是通过Comparable接口里的(int compareTo(Object[] obj); 实现的, 那么每次实体类实现接口compareTo方法时, 第一时间都要做一次类型强转如: Dog d=(Dog) obj; 那么有没有办法能避免这种冗余操作呢? 聪明的你可能已经想到了使用泛型即可完成这种功能.

所以重新调整上面的代码:

step 1. 接口泛型化

1
2
3
4
5
package com.lee.tank.designPatterns.Strategy;

public interface Comparable<T> {
int compareTo(T t);
}

step 2. Sort (和上一版本保持一致即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.lee.tank.designPatterns.Strategy;

public class Sorter{

public void sort(Comparable[] arr) {
for(int i=0; i<arr.length - 1; i++) {
int minPos = i;

for(int j=i+1; j<arr.length; j++) {
minPos = arr[j].compareTo(arr[minPos])==-1 ? j : minPos;
}
swap(arr, i, minPos);
}
}

private void swap(Object[] arr, int i, int j) {
Object temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

step 3. 实体类泛化

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
30
package com.lee.tank.designPatterns.Strategy;

public class Dog implements Comparable<Dog>{ // Comparable<Dog> 泛型填充具体的Dog
int height ;
int weight ;

public Dog(int height, int weight) {
this.height = height;
this.weight = weight;
}

@Override
public String toString() {
return "Dog{" +
"height=" + height +
", weight=" + weight +
'}';
}

@Override
public int compareTo(Dog d) {
if(this.weight<d.weight){
return -1;
}else if(this.weight>d.weight){
return 1;
}else{
return 0;
}
}
}

step 4 调用验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.lee.tank.designPatterns.Strategy;

import java.util.Arrays;

public class Strategy {
public static void main(String[] args) {
Sorter sorter = new Sorter();
Cat[] cats={new Cat("Alex",3), new Cat("Brox",2),new Cat("Joshua",4), new Cat("Ted",1)};
sorter.sort(cats);
System.out.println(Arrays.toString(cats));

Dog[] dogs={new Dog(5,10),new Dog(7,8),new Dog(6,9)};
sorter.sort(dogs);
System.out.println(Arrays.toString(dogs));

}
}

发现也能实现最终排序功能

策略_2适配Object排序

策略

截止目前, 虽然对比较类型做了泛化让其适配所有类型, 只要该类型实现了Comparable接口及其compareTo方法即可. 但是灵活性还是不够高, 如果想使用身高height(而不是原来的体重weight)对狗进行比较, 即提高比较本身灵活性, 改变比较的策略, 又该怎么做呢 ? 这才真正引出策略模式.

那么如何才能根据用户需求进行个性化的比较策略的切换以期达到灵活比较的目标呢? 这就需要用户将策略类型作为参数传进比较方法.

Step 1. 定义Comparator接口

1
2
3
4
5
package com.lee.tank.designPatterns.Strategy;

public interface Comparator<T> { //也需要泛化, 因为类型的匹配还是需要的
public int compare(T o1, T o2);
}

Step 2. 定义策略类型

需要什么类型就定义什么类型比如下面定义了两种, DogComparator1 按体重weight比较.DogComparator2 按体重height比较.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.lee.tank.designPatterns.Strategy;

/* 策略一: weight*/
public class DogComparator1 implements Comparator<Dog> { //DogComparator1具体比较策略的实现, 按体重
@Override
public int compare(Dog dogA, Dog dogB) {
if (dogA.weight < dogB.weight) {
return -1;
} else if (dogA.weight > dogB.weight) {
return 1;
} else {
return 0;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.lee.tank.designPatterns.Strategy;

/* 策略一: height*/
public class DogComparator2 implements Comparator<Dog> { //DogComparator2具体比较策略的实现, 按身高
@Override
public int compare(Dog dogA, Dog dogB) {
if (dogA.height < dogB.height) {
return -1;
} else if (dogA.height > dogB.height) {
return 1;
} else {
return 0;
}
}
}

Step 3 Sorter方法应用策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.lee.tank.designPatterns.Strategy;

public class Sorter<T> {
public void sort(T[] arr, Comparator<T> ct) { // sort方法里定义了不同的策略类型comparator参数
for (int i = 0; i < arr.length - 1; i++) {
int minPos = i;

for (int j = i + 1; j < arr.length; j++) {
//minPos = arr[j].compareTo(arr[minPos])==-1 ? j : minPos;
minPos = ct.compare(arr[j],arr[minPos])==-1 ? j : minPos; //注意这里比较是用comparator的compare方法进行比较.
}
swap(arr, i, minPos);
}
}
private void swap(T[] arr, int i, int j) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

Step 4. 调用验证

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
package com.lee.tank.designPatterns.Strategy;

import java.util.Arrays;

public class Strategy {
public static void main(String[] args) {
Sorter<Dog> dogSorter = new Sorter<>();
Dog[] dogs={new Dog(5,10),new Dog(7,8),new Dog(6,9)};
dogSorter.sort(dogs, new DogComparator1()); // 排序时需要什么策略就传什么策略
System.out.println("按1 weight排序:\n"+Arrays.toString(dogs));

dogSorter.sort(dogs, (dogA,dogB)->{ //因为comparator只有一个方法, 所以也可以用lambada表达式的写法传入第二个参数
if (dogA.height < dogB.height) {
return -1;
} else if (dogA.height > dogB.height) {
return 1;
} else {
return 0;
}
});
System.out.println("按2 height排序:\n"+Arrays.toString(dogs));


}
}

策略类型

总结

策略模式总结

工厂模式

门面

装饰器

责任链

观察者

组合模式

享元

代理(静/动)

迭代器

访问者

构建器

适配器

桥接

命令

原型(prototype)

备忘录

模板

状态

解释器

总结和6大设计原则

-------------本文结束感谢您的阅读-------------