共计 9548 个字符,预计需要花费 24 分钟才能阅读完成。
说到设计模式,先本人回顾下看能想出几种进去,而后每种的实现形式是否还记得。说实话,整顿完这篇文章之前也说不上多少个。说不上来几种的无非有两种,一种是真的不会,就记得几个;一种是十分熟了,可能做到手中无剑心中有剑的境界,尽管说不上来,然而写代码的时候不必刻意去想用哪种设计模式,自然而然就写出颇为完满的代码。当然,咱们都要成为第二种,既然要成为第二种,那么就从好好看这篇设计模式开始吧!
设计模式(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 值了,代码也简洁了不少。
参考:《2020 最新 Java 根底精讲视频教程和学习路线!》
链接:https://juejin.cn/post/691351…