设计模式(一)

InterviewCoder

# 设计模式笔记 __Brath.Li

# GoF23:23 种设计模式

设计模式的本质是面向对象设原则的实际运用,是对类的封装性,继承性,多态性以及类的关联关系和组合关系的充分理解。

正确使用设计模式具有以下优点:

1. 提高程序员思维能力,编程能力和设计能力

2. 使程序设计更加标准化,代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开的周期。

3. 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

# 创建型模式:

单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式。

# 结构型模式:

适配器模式、桥接模式、装饰模式、组合模式、外欧冠模式、享元模式、代理模式。

# 行为性模式:

模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、责任链模式、访问者模式。

# OOP 面向对象七大原则:

1.OCP 开闭原则:

软件实体(包括类、模块、功能等)应该对扩展开放,但是对修改关闭。

2. 里氏替换原则:

继承必须确保超类锁拥有的性质在子类中仍然成立。

3. 依赖倒置原则:

面向接口编程,不要面向实现编程。

4. 单一职责原则:

控制类粒度大小、将对象解耦合、提高内聚性。

5. 接口隔离原则:

要为各个类建立他们需要的专用接口。

6. 迪米特法则:

​ ** 只与你的直接朋友交谈、不跟 “陌生人” 说话。 **

7. 合成复用原则:

尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

# 单例设计模式:

作用:让一个类只能创建一个实例(因为频繁的创建对象,回收对象会造成系统性能下降。)。解决对象的唯一性,保证了内存中一个对象是唯一的 。

# 饿汉式:Hunary

饿汉式

步骤:1. 私有化构造器 2. 直接创建静态对象 3. 创建一个静态的方法,供外部调用实例

优点:类初始化的时候,会立即加载该对象,线程天生安全,调用效率高。

缺点:无法避免被反射破解,不安全

# 懒汉式:LazyMan

image-20210910151318667

步骤:1. 私有化构造器,不会直接创建对象 2. 向外暴露调用对象方法,在方法中先进行对象判空,为空才创建对象,这就是懒汉式

优点:类初始化时,不会初始化该对象,真正需要使用的时候才会去创建该对象,具备懒加载功能。

缺点:

1. 判空浪费时间

2. 不加 Synchronized 的懒汉式线程不安全,需要用到 volatile 关键字保持 new 对象的原子性一致

懒汉式单线程下是 OK 的,但是多线程并发下不安全。

image-20210910153316119 开启十次线程测试

image-20210910153327761

每次重启线程数都不一致,线程不安全。

怎么解决:

Tips:懒汉式也是可以实现线程安全的:只要加上 Synchronized 加锁即可:

但是这样一来,会降低整个访问的速度,而且每次都要判断。那么有没有更好的方式来实现呢?

解决方案:

double-check-lock 双重加锁

image-20210910152638263

所谓双重加锁机制。指的是,并不是每次进入 getInstance 方法都需要同步,而是先不同步,进入方法之后先检查实例是否存在,如果不存在才进入下面 Synchronized 加锁,之后会再次检查实例是否存在,如果还不存在才创建实例。

双重检测方式(因为 JVM 本身重排序的原因,可能会出现多次的初始化)

这种模式下的懒汉式,称为 DCL 懒汉式

你以为这样就没问题了?大错特错,注意:这里的 new 对象操作不是原子性的

image-20210910153630120

new 一个对象的执行顺序 ↓

1
2
3
4
5
6
7
8
9
10
11
/**
* 1.分配内存空间
* 2.执行构造方法初始化对象
* 3.把这个对象指向内存空间
* 这是创建对象的步骤 正确顺序为 123
* 举例:
* 线程A 创建对象 步骤为 123, 我们的代码没有问题
* 线程B 创建对象 因为操作步骤不是原子性的,可能会走成 1 3 2,先指向了内存空间,再去构造对象
* 这时会出现一个问题,指令重排:双重检测模式会失效,因为此时我们的类,不为空了,但是里面返回的对象是空的。
* 所以就要用到一个关键字volatile,避免指令重排,保持操作原子性
*/

image-20210910153756918

# 静态内部类模式:

image-20210910155204370

步骤:1. 私有化构造器 2. 创建一个静态内部类,类中创建外部类的实例 3. 向外暴露一个方法获取静态内部类中创建的对象

优点:结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。

缺点:每次调用都会创建多余的对象

# 以上单例设计模式都会被反射破解,枚举不会!

枚举:枚举本身是一个类,继承了 Enum 的实例就成为了枚举类

使用枚举实现单例模式,实现简单、调用效率高,枚举本身就是单例,由 JVM 从根本上提供保障,避免通过反射和反序列化的漏洞,缺点是没有延迟加载。

在源码 Constructor 中image-20210910150042754

如果是对象是通过反射机制创建的会抛出一个异常

IllegalArgumentException: Cannot reflectively create enum objects

枚举类在 traget 输出的代码中,构造器是空的,隐藏起来了,用 javap -p 反编译也看不见构造器

image-20210910150706569

我们用反射机制获取空构造器会获取不到对象,这时候用 jad 反编译工具,得到的 java 文件中,

image-20210910150400927

可以看见构造器其实是有数据的,我们把数据放到反射中实现,就会得到这个异常 Cannot reflectively create enum objects

image-20210910150429958

image-20210910150418256

由此可见,反射不能破坏枚举的单例模式

优点:实现简单,线程安全,防止反射攻击等。

缺点: 在不需要的时候可能就加载了,造成内存浪费

Tips:利用反射破解懒汉式。懒汉式不是安全的!

image-20210910160648326

私有化构造器的时候判断外面的字段 guoqing 是否 ==false,等于的话就设置为 true,如果不等于的话就抛出运行异常,这样设计的话在我们用反射机制获取构造器的时候,就获取不到对象了

但是有解决方法:利用反射强大的机制,假如我们反编译,知道要破解的字段是 guoqing,那我们直接用反射获取 guoqing 字段,然后设置解除私有化限制,在创建构造器实例的时候,把 guoqing 设置为 true,就可以破解 DCL 懒汉式了

image-20210910160638735

# 工厂设计模式 Factory Model:

核心本质:

实例化对象不使用 new,用工厂方法代替

将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。

详细分类:

简单工厂模式

用来生产同一等级结构中的任意产品:扩展性差

工厂方法模式

用来生产同一等级结构中的固定产品:扩展性强

抽象工厂模式

围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。

工厂设计模式应用场景:

JDK 中的 Calendar 的 getInstance 方法

JDBC 中的 Connection 对象的获取

Spring 的 IOC 容器创建管理 Bean 对象

反射 Class 对象的 newInsetance 方法

# 建造者模式:

建造者模式也属于创建型模式,它提供了一种创建对象的最佳方式。

定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

主要作用:在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂对象。

用户只需要给出指定复杂对象的类型和内容,建造者模式负责将按顺序创建复杂对象(把内部的建造过程和细节隐藏起来)

例子:

工厂(建造者模式):负责制造汽车(组装过程和细节在工厂内)

汽车购买者(用户):你只需要说你需要的型号(对象的类型和内容),然后直接购买就可以了(不需要知道汽车是怎么组装的(发动机、变速箱、轮毂、车门))

# 原型模式:

​ 创建型模式之一。

# 适配器模式

​ 将一个类的接口转换成客户希望的另外一个接口,Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作!

​ 角色分析:

​ 1 . 目标接口:客户所期待的接口,目标可以使具体或抽象的类,也可以是接口。

​ 2 . 需要适配的类:需要适配的类或者适配者类。

​ 3 . 适配器:通过包装一个需要适配的对象,将原接口转换成目标对象。

​ 对象适配器优点

​ 1 . 一个对象适配器可以把多个不同的适配者适配到同一个目标

​ 2 . 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据 “里氏代换原则” ,适配者的子类也可以通过该适配器进行适配。

​ 类适配器缺点:

​ 1 . 对于 Java、C# 等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者。

​ 2 . 在 Java、C# 等语言中,适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。

​ 适用场景:

​ 1 . 系统需要使用一些现有的类,而这些类的接口不符合系统的需要,甚至没有这些类的代码。

​ 2 . 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

评论