说到设计模式,先自己回忆下看能想出几种出来,然后每种的实现方式是否还记得。说实话,整理完这篇文章之前也说不上多少个。说不上来几种的无非有两种,一种是真的不会,就记得几个;一种是非常熟了,能够做到手中无剑心中有剑的境界,虽然说不上来,但是写代码的时候不用刻意去想用哪种设计模式,自然而然就写出颇为完美的代码。当然,我们都要成为第二种,既然要成为第二种,那么就从好好看这篇设计模式开始吧!
设计模式(Design Pattern)是一套被反复使用,多数人知晓的,经过分类编写的目的,代码设计经验的总结。使用设计模式是为了可重用代码,让代码更容易被他人理解,保证代码可靠性。毫无疑问,设计模式于己于人于系统都是多赢的。设计模式使代码编制真正工程化。
设计模式
首先我们需要整理一下有哪几种设计模式,上图:
看到原来有这么多设计模式,脑子是不是有些发蒙,不过没关系,接下来小菜带你一个一个攻破!
一、创建型模式
创建型模式的主要关注点在于 如何创建对象
,它的主要特点是:将对象的创建与使用分离。这样做的好处便是:降低系统的耦合度,使用者不需要关注对象的创建细节。其中创建者模式又分为以下几种:
1)单例模式
说到单例模式,是不是都感觉自信起来了。没错这个平时说的最多,用到最多,面试中问的也最多的设计模式。
单例模式(Singleton Pattern) 是 Java 中最简单的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象会被创建。这样子我们就不需要实例化该类的对象,可以直接访问。简单来说:就是这个模式是负责 计划生育 的,只允许创建一个对象,不允许有太多的徒子徒孙。
单例模式 又分为两种实现方式:
- 饿汉式:类加载就会导致该单例对象被创建
- 懒汉式:类加载不会导致该单例对象被创建,而是首次使用该对象时才会创建
接下来我们就详细介绍下两种不同的实现方式
1. 饿汉式
静态变量方式
:
public class Singleton {
// 步骤1 :私有的构造方法
private Singleton() { }
// 步骤2 :在成员位置创建该类的对象
public static Singleton singleton = new Singleton();
// 步骤3 :对外提供静态方法获取该对象
public static Singleton getInstance() {
return singleton;
}
}
复制代码
三步走战略将 单例模式 显得格外简单。该方式中 singleton
对象是随着类的加载而创建的,弊端也很明显:如果该对象足够大,而一直没有被使用到,那么就会造成内存的浪费
静态代码块方式:
public class Singleton {
// 步骤1 :私有的构造方法
private Singleton() { }
// 步骤2 :在成员位置创建该类的对象
public static Singleton singleton;
{
singleton = new Singleton();
}
// 步骤3 :对外提供静态方法获取该对象
public static Singleton getInstance() {
return singleton;
}
}
复制代码
该方式与上一种 静态变量
的方式大同小异,该对象的创建也是随着类的加载而创建的,弊端当然也是一样的。
2. 懒汉式
线程不安全:
public class Singleton {
// 步骤1 :私有的构造方法
private Singleton() { }
// 步骤2 :在成员位置创建该类的对象
public static Singleton singleton;
// 步骤3 :对外提供静态方法获取该对象
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
复制代码
上面代码中我们在成员位置声明 Singleton 类型的静态变量,并没有进行对象的赋值操作,而是当调用 getInstance()
方法时才创建 Singleton 类的对象,这种方式便实现了懒加载的效果,但是弊端便是:多线程环境下,会出现线程安全的问题
线程安全:
提到线程安全问题,我们第一反应便是想到利用 synchronized
对共享变量进行上锁,那么便有了以下代码:
public class Singleton {
// 步骤1 :私有的构造方法
private Singleton() {}
// 步骤2 :在成员位置创建该类的对象
public static Singleton singleton;
// 步骤3 :对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
复制代码
该方式也实现了 懒加载 的效果,同时也解决了线程安全的问题。看似完美的代码,那么能否进行进一步的优化。我们都是到经过 synchronized 关键字上锁,执行效率也会变低。上锁的关键在于为了重复创建对象,如果对象已经创建,我们就不需要上锁,那么我们是否可以将 锁细化,不需要把锁加在方法上,而是在对象为空时才加上锁,那么就有了第三种方式.
双重检查锁:
public class Singleton {
// 步骤1 :私有的构造方法
private Singleton() {}
// 步骤2 :在成员位置创建该类的对象
public static Singleton singleton;
// 步骤3 :对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,当 singleton 为空时加锁,否则直接返回实例
if (singleton == null) {
synchronized (Singleton.class) {
//第二次判断,当抢到锁时再判断 singleton 是否为空
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
复制代码
看完代码你可能会觉得奇怪,为什么加完锁后还要判断一次是否为空呢,不都已经加上锁,这个时候肯定没人跟自己抢了,再判断一次不是多此一举吗。
在并发环境下,线程A 和 线程B 通过进入 第一步
后,线程A 比 线程B 先获取到锁,这个时候 线程B 会在等待队列中等待获取锁,如果这个时候没有 第三步
的判空,当 线程A 执行完释放锁,线程B 这个时候已经进入了 第一步
,它这个时候还以为对象还没有被创建,等到它获取到锁就会又创建一个类对象,这样子就不符合单例模式了。因此 第三步
也是至关重要的。
到现在为止,你应该又有了 该代码已经完美 的想法了,但是如果对之前写的并发文章有熟悉的小伙伴就会知道 并发中还会存在一个 指令重排
的情况,下面我们来看一下
正常情况:
memory=allocate(); //1:分配对象的内存空间
cteateInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
复制代码
指令重排:
memory=allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
//注意,此时对象还没有被初始化!
cteateInstance(memory); //2:初始化对象
复制代码
由于单线程内要遵守 intra-thread semantics,从而能保证 线程A 的执行结果不会被改变。但是,当 线程A 和 线程B 按上图顺序执行时,线程 B 将看到一个还没有被初始化的对象。要解决这个问题,有两个方法:
- 不允许 步骤2 和 步骤3 进行重排序
- 允许 步骤2 和 步骤3 进行重排序,但是不允许其他线程看到这个重排序
解决思路有了,我们自然会联想到使用 volatile
这个关键字,这个关键字的作用之一便是禁止指令重排,代码也十分简单,只需要使用 volatile
修饰成员变量即可:
public class Singleton {
// 步骤1 :私有的构造方法
private Singleton() { }
// 步骤2 :在成员位置创建该类的对象
// 使用 volatile 修饰
public static volatile Singleton singleton;
// 步骤3 :对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,当 singleton 为空时加锁,否则直接返回实例
if (singleton == null) {
synchronized (Singleton.class) {
//第二次判断,当抢到锁时再判断 singleton 是否为空
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
复制代码
添加 volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。这下总算完美了,但是完美之后我们就要思考还有没有其他方式也能完美的实现单例模式,答案肯定是有的。
静态内部类方式:
上面利用 饿汉式
创建单例,由于对象是随着类的加载而创建的,会占用内部空间而选择了 懒汉式
,但是饿汉式
也是有改进的地方。我们可以通过内部类创建类对象,由于 JVM 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用的时候才会被加载,并初始化其静态属性,而且静态属性被 static
修饰,保证只被实例化一次,并且在没有加任何锁的情况下,保证了多线程下的安全,没有任何性能影响和空间的浪费,所以这也是一种实现单例的好方式。
public class Singleton {
// 步骤1 :私有的构造方法
private Singleton() { }
private static class SingletonInner{
private static final Singleton SINGLETON = new Singleton();
}
public static Singleton getInstance() {
return SingletonInner.SINGLETON;
}
}
复制代码
枚举方式:
public enum Singleton{
INSTANCE;
}
复制代码
从代码上就可以看出这是一种极为简单的单例实现模式,也是极力推荐的。因为枚举类型是线程安全的,并且只会装载一次,而且枚举类型是所有单例实现中唯一一种不会被破坏的单例实现模式。这种方式也是属于 饿汉式
方式。
2)工厂模式
Java 是面向对象开发的,万物皆对象 便是 Java 中的一个理念。对象都是需要创建的,如果我们都是通过 new 方式来创建一个对象,假如我们的类名做了部分修改,那么是否所有通过 new 方式创建对象的地方都需要修改一遍,这显然违背了软件设计中的开闭原则,耦合将会十分严重。
生活中存在工厂的概念,那么我们设计模式也应当有 工厂模式。我们可以通过使用工厂来生产对象,类如果发生变更,我们可以不用理会,我们只和工厂打交道就行。这样就达到了对象解耦的目的,所以 工厂模式 设计的初衷便是为了:解耦
根据需求的不同,工厂模式中又分为三种分别是:
- 简单工厂模式
- 工厂方法模式
- 抽象方法模式
1. 简单工厂模式
简单工厂模式在开发中用到最多,可能你有时候都没发现自己使用的是简单工厂模式
我们举个例子来认识一下 简单工厂模式 :
有一家奶茶店,里面卖着 草原奶茶 和 椰香奶茶,那么根据顾客不同的喜好,奶茶店就需要制作不同的奶茶。
一般来说我们惯性思维便是创建两个奶茶类 GrasslandsMilk
、CoconutMilk
,然后需要哪种奶茶就 new 哪种。
使用 简单工厂模式来实现是比较简单的,首先我们需要知道 简单工厂模式 中存在这哪几种角色:
- 抽象产品: 定义产品的规范,描述产品的主要特性和功能
- 具体产品: 实现或者继承抽象产品的子类
- 具体工厂: 提供创建产品的方法,调用者通过该方法来获取产品
然后我们给每个角色归类一下:草原奶茶 和 椰香奶茶 我们可以归到 具体产品
中,然后我们可以抽取一个 奶茶 的抽象类出来归到 抽象产品
中,最后创建一个制作奶茶的类,归到具体工厂
中
图示如下:
码示如下:
public class SimpleMilkFactory {
public Milk createMilk(String type) {
Milk milk = null;
if (StringUtils.equals("grasslands", type)) {
milk = new GrasslandsMilk();
} else if (StringUtils.equals("coconut", type)) {
milk = new CoconutMilk();
}
return milk;
}
}
复制代码
通过代码可以看到我们可以通过传入的type
进行判断需要生产何种奶茶,客户端就不需要字节创建 奶茶,直接从工厂中获取即可。但是虽然解除了客户端和奶茶类的耦合,又增加了工厂和奶茶类的耦合,后期如果需要增加新品种的奶茶,我们就得修改工厂中的获取方法,还是违反了开闭原则。
既然工厂和具体产品类产生了耦合,那我们是否考虑 "术业有专攻"
的理念,每个产品都要有它对应的生产工厂,比如一个鞋厂,里面有 运动鞋 和 休闲鞋,那么应该需要一个 运动鞋工厂 和一个 休闲鞋工厂 ,这样子就解决了工厂和具体产品类 之间的耦合。有想法就有办法,这个时候就有了 工厂方法模式
2. 工厂方法模式
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类
简而言之,工厂方法模式 便是在 简单工厂模式 基础上增加了一个 抽象工厂 的角色
- 抽象工厂: 提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
图示如下:
码示如下:
抽象工厂:
public abstract class MilkFactory {
abstract Milk createMilk();
}
复制代码
具体工厂:
/**
* 草原奶茶工厂
*/
public class GrassLandsMilkFactory extends MilkFactory{
@Override
Milk createMilk() {
return new GrasslandsMilk();
}
}
/**
* 椰香奶茶工厂
*/
public class CoconutMilkFactory extends MilkFactory {
@Override
Milk createMilk() {
return new CoconutMilk();
}
}
复制代码
奶茶店类:
public class MilkStore {
private MilkFactory milkFactory;
public MilkStore(MilkFactory milkFactory) {
this.milkFactory = milkFactory;
}
public Milk getMilk() {
return milkFactory.createMilk();
}
}
复制代码
通过以上设计模式我们就将 具体产品 和 工厂类 脱耦出来了,每个 产品 都对应自己的 工厂,我们如果增加某种产品就不用修改 原工厂 ,而是添加 具体产品和对应的具体工厂类。
这种方法虽然解耦了,但是弊端也是很明显的,那就是增加了系统复杂度,每增加一个产品就需要增加一个具体的产品类和具体的工厂类,这好像也有些违背我们程序员的 "懒人开发原则"
。还是老样子,有想法就与方法,这个时候又有了 抽象工厂模式
3. 抽象工厂模式
工厂方法模式 产生的缺点便是它太专一了,一个工厂只生产一种产品,这不是把路走窄了吗!听到这,你们估计都想骂"渣男"
了,前面说 工厂方法模式 好的是你,现在又嫌人家太专一了。其实不然,工厂方法的专一确实是有好处的,但是不能过于极致,也不能过于泛滥,不能说一个工厂什么都生产,那么将毫无意义,又回到原点。
针对一个产品生产过于专一,但我们可以针对一个产品族进行生产
产品族值得便是同一类型的产品,比如衣服、裤子这类就是同一类型的产品,简单来说就是一条龙服务,一家奶茶店我们可以卖奶茶还可以买甜点,这样子客户就可以不用去别家买完蛋糕再来奶茶店买奶茶了。
抽象工厂模式 的角色和 工厂方法模式 一致,分为以下几种:
- 抽象工厂: 提供了创建产品的接口,包含多个创建产品的方法
- 具体工厂: 实现抽象工厂中多个抽象方法,完成具体产品的创建
- 抽象产品: 定义产品的规范,描述产品的主要特性和功能
- 具体产品: 实现抽象产品中定义的接口
图示如下:
码示如下:
抽象工厂:
public interface DessertFactory {
Milk createMilk();
Cake createCake();
}
复制代码
具体工厂:
/**
* 草原风格甜点工厂
*/
public class GrassLandsDessertFactory implements DessertFactory {
@Override
public Milk createMilk() {
return new GrasslandsMilk();
}
@Override
public Cake createCake() {
return new GrasslandsCake();
}
}
/**
* 椰香风格甜点工厂
*/
public class CoconutDessertFactory implements DessertFactory {
@Override
public Milk createMilk() {
return new CoconutMilk();
}
@Override
public Cake createCake() {
return new CoconutCake();
}
}
复制代码
这样子如果加同一个产品族的话,只需要添加一个对应的工厂类即可,不需要修改其他的类,但是这个设计模式也是存在部分缺点的,那就是如果产品族中需要添加一个产品,那么还是需要修改产品族工厂 的,但是金无足赤,我们能做的便是不断完善。
3)原型模式
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象
原型模式存在的角色相对来说比较简单,一个 抽象原型接口 和一个 具体实现类
在原型模式中我们又可以分为:浅克隆
和 深克隆
两个概念
- 浅克隆: 创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性, 仍指向原有属性所指向的对象的内存地址
- 深克隆: 创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象的地址
1. 浅克隆
在 Java 中我们最简单的使用方法便是实现 Cloneable 接口,然后重写 clone()
方法 :
那么这种方式是属于深克隆还是浅克隆呢,我们写个小示例测试一下:
从示例上看好像也成功实现了克隆的效果,也支持修改成员变量,但是示例中的 name
是 String
类型的,我们将其换成对象类型 Person
再试下:
我们看到结果有些许不对劲,怎么 好人卡 都是属于 小王
的?其实这就是浅克隆的效果,对具体原型类中的引用类型的属性进行引用的复制。这种情况下我们就要使用 深克隆 来帮忙了。
2. 深克隆
方式 1: 手动为引用属性赋值
我们只需要修改克隆方法即可
public NiceCard clone() throws CloneNotSupportedException {
NiceCard niceCard = (NiceCard) super.clone();
// 手动为引用属性赋值
niceCard.setPerson(new Person());
return niceCard;
}
复制代码
显然,这种手动的方式在关联对象少的情况是可取的,假设关联的对象里面也包含了对象,就需要层层修改,比较麻烦。不推荐这样使用!
方式 2: 借助 FastJSON
这种方法很简单,也不用实现 Cloneable
接口,是一种好的解决方法
方式 3: 使用 java 流的序列化对象
我们可以创建一个序列化工具类:
然后我们就可以使用工具类来实现克隆:
以上便是深克隆的三种方法,最后一种方式代码虽然比较多,但是比较高效和容易抽象,也是比较常用的方式。
4)建造者模式
将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示
通俗来说就是,将构造的行为与装配的行为相分离,从而可以构造出复杂的对象,这个模式适用于:某个对象的构建过程复杂的情况
由于事先了构建和装配的解耦,不同的构建器,相同的装配,便可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无需知道其内部具体构造细节。
建造者模式有如下角色:
- 抽象建造者类: 这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建
- 具体建造者类: 实现抽象建造者类,完成复杂产品各个不见的具体创建方法,在构造过程完成后,提供产品的实例
- 产品类: 要创建的复杂对象
- 指挥者类: 调用具体建造者来创建复杂对象的各个部分,在指挥者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建
图示如下:
我们举个例子说明一下:自行车包含了车架,车座等组件的生产,其中车架可以是 碳纤维 和 铝合金等材质,车座可以是 橡胶,真皮等材质。不同厂商可以生产不同的自行车,这种生产模式就可以使用建造者模式。已知有两个厂商:GradeBuilder(高档厂商) 和 NormalBuilder(普通厂商) ,和一个DirectorStore(自行车卖家),我们老样子来归类一下:
- 具体建造者: GradeBuilder(高档厂商) 和 NormalBuilder(普通厂商)
- 产品类: Bike(自行车)
- 指挥者类: DirectorStore(自行车卖家)
码示如下:
看完上面代码,我们也盘下 建造者模式 的优缺点:
优点:
- 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
- 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
- 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
- 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。
缺点:
建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
扩展
有用过 Lombok 的小伙伴,都会觉得真香
,但是不知道你们有没有用过 Lombok 里面的一个注解@Builder
,具体用法如下:
@Builder
public class Computer {
private String cpu;
private String hardDisk;
private String memory;
public static void main(String[] args) {
Computer.builder().cpu("英特尔")
.hardDisk("希捷")
.memory("金士顿")
.build();
}
}
复制代码
是不是感到挺好用的,其实这就是用到建造者模式,那我们自己手动实现一下:
使用这种方式我们就可以不用 new 一个对象出来,再一个个 set 值了,代码也简洁了不少。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。