一、设计模式简介
1.1 什么是设计模式?
- 设计模式(Design Pattern)是前辈们对开发经验的总结,是解决特定问题的一系列套路。他不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
- 1995年,GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收了23种设计模式(GoF23),从此树立了软件设计模式的里程碑,人称DoF设计模式。
1.2 学习设计模式的意义
- 设计模式的本质是面向对象原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
- 正确使用设计模式有以下优点:
- 可以提高程序员的思维能力、编程能力和设计能力
- 使程序设计更加标准化、代码编制更加工程化,是软件开发效率大大提高,从而缩短软件的开发周期
- 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强
1.3 Fof23
一种思维、一种态度、一种进步
创建型模式(让对象的创建和使用分离):
单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式
结构型模式(让类或对象按照某种布局组成一些更大的结构):
适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式
行为型模式(描述类或对象如何相互协作去完成单个对象无法完成的任务):
模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式
二、OOP(面向对象)七大原则
- 开闭原则:对扩展开放,对修改关闭
- 里氏替换原则:继承必须确保超类所拥有的性质在子类中依旧实现(不修改父类的方法)
- 依赖倒置原则:要面向接口编程,不要面向实现编程
- 单一职责原则:控制类的粒度大小、将对象解耦、提高其内聚性(一个方法做一件事)
- 接口隔离原则:要为各个类建立它们需要的专用接口(降低耦合性)
- 迪米特法则:只与你的直接朋友交谈,不和“陌生人”说话(类似于AOP)
- 合成复用原则:尽量先使用组合或聚合等关联关系来实现,其次才考虑使用继承关系来实现
创建型模式
三、单例模式
最重要的思想:构造器私有
一旦私有,别人就无法new这个对象,保证对象是唯一的
3.1 饿汉式单例
//饿汉式单例
public class Hungry {
//饿汉式一上来就把类中的所有东西加载到内存,可能会浪费空间
private byte[] bytes1 =new byte[1024*1024];
private byte[] bytes2 =new byte[1024*1024];
private byte[] bytes3 =new byte[1024*1024];
/**
* 构造器私有,无法new这个对象,保证内存中只有一个对象
*/
private Hungry(){
System.out.println("123");
}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
3.2 懒汉式单例
//懒汉式单例
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMan lazyMan;
/**
* 单线程下ok,但是多线程并发就不行了
* @return
*/
public static LazyMan getInstance(){
if(lazyMan==null){
lazyMan = new LazyMan();
}
return lazyMan;
}
//多线程下无法实现单例
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
为了防止这种情况发生,有了以下改进:
//懒汉式单例 DCL懒汉式 双重检测锁模式
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private volatile static LazyMan lazyMan; //一定要加上volatile,防止指令重排
/**
* 单线程下ok,但是多线程并发就不行了
* 在多线程下需要加锁
* 双重检测锁模式
* @return
*/
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan = new LazyMan(); //不是一个原子性操作
/**
* 1、分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 可能发生指令重排
* 正常情况 123
* 可能发生 132 (也能运行) A线程
* 但是在3这一步,对象指向了空间,但是这个对象没有初始化
* 那么线程B会认为lazyMan不为空而返回一个空的对象
*/
}
}
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
3.2.1 synchronized(简要理解)
实现线程同步,让多个线程排队依次获取某个资源(竞争资源),保证数据不会出错
多个线程不竞争资源的时候,就不会出现排队(同步)的情况
会出现很多情况,重要是理解锁定的是什么元素,同一个就会同步,不是同一个就不会同步
synchronized 到底锁定的是哪个元素?
- 修饰方法
- 静态方法 (锁定的是类)
- 非静态方法(锁定的是方法的调用者)
- 修饰代码块(锁定的是传入的对象)
//修饰代码块
for(int i=0;i<5;i++){
new Thread(()->{
Integer num = 1;//-128~127是常量池 ,其他范围会出现竞争
//Integer num = new Integer(1); //不会出现竞争
data.func(num);
}).start();
}
3.2.2 volatile (特征修饰符)
volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值
那么什么情况下会发生指令重排?
指令重排是发生在cpu指令集的,当cpu发现两条指令的执行顺序对结果没有影响时,就有可能发生指令重排。
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan = new LazyMan(); //不是一个原子性操作,涉及cpu的操作
}
}
}
return lazyMan;
}
来看之前写的这段代码,写完似乎没有啥问题了,但是new操作不是一个原子性操作,可能发生指令重排(具体步骤前面解释了)
正常的new操作是 1 -> 2-> 3 执行的,但是 1 -> 3 -> 2 执行后,也是正常的,所以这里会发生指令重排
那么为什么会出问题?
当发生指令重排,执行 1 3 2 操作时,在3这个步骤:把这个对象指向这个空间
在多线程的情况下,其他的线程会认为此时 lazyMan 不为空(最外层的if),就直接返回了
但是这个对象还是空的,所以就出错了
3.2.3 为什么需要双重检测?
我去掉最外层的if后,难道就不可以运行了吗?这样是不是就可以不使用volatile了?
答案:一旦synchronized被正确初始化后,内存的if就一定是false,但是我们并不需要每次都通过上锁来判断,所以双重锁的目的是为了成功初始化synchronized后,不再触发加锁操作
3.3 静态内部类
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.holder;
}
public static class InnerClass{
private static final Holder holder = new Holder();
}
}
3.4 反射
以上的单例模式都是不安全的
public static void main(String[] args) {
LazyMan lazyMan1 = LazyMan.getInstance();
LazyMan lazyMan2 =LazyMan.getInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
lazyMan1 和 lazyMan2 都是相同的对象
(1)通过 newInstance() 创建对象
适用于无参构造方法,并且构造方法需要是public,这里无法使用
LazyMan lazyMan1 = LazyMan.class.newInstance();
(2)通过 getConstructor() 或 getDeclaredConstructor() 获取构造器对象
并调用其newInstance方法创建对象
适用于无参和有参构造方法
getDeclaredConstructor()
这个方法会返回制定参数类型的所有构造器,包括public和非public的(当然有private)
getConstructor()
只返回制定参数类型访问权限是public的构造器
public static void main(String[] args) throws Exception {
LazyMan lazyMan1 = LazyMan.getInstance();
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true); //无视了私有的构造器,必须要加,否则报错
LazyMan lazyMan2 = constructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
(3)如何防止上述的操作
由于上述操作主要是无视了构造方法的权限private,但最终还是通过这个构造方法来创建对象的
所以我们可以在构造方法中加一层锁
private LazyMan(){
synchronized (LazyMan.class){
if(lazyMan!=null){ //构造方法已经执行后,如果再次访问,说明不怀好意
throw new RuntimeException("不要试图使用反射破坏");
}
}
}
再次运行时:
public class Test {
public static void main(String[] args) throws Exception {
LazyMan lazyMan1 = LazyMan.getInstance();
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true); //无视了私有的构造器
LazyMan lazyMan2 = constructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
}
(4)那么有没有方法继续绕过呢?
其实换个顺序就行了
public class Test {
public static void main(String[] args) throws Exception {
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true); //无视了私有的构造器
LazyMan lazyMan2 = constructor.newInstance();
LazyMan lazyMan1 = LazyMan.getInstance();
//LazyMan lazyMan1 = constructor.newInstance(); 也可以继续使用构造方法创建对象
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
}
因为通过构造方法直接创建对象后,lazyMan这个值始终为空,也就绕过了这个检测
(5)那么如何防范?
单例中加上一个私有静态标志位就行了
private static boolean flag = false;
private LazyMan(){
if(flag==false){
flag = true; //这样就只能访问一次构造器
}else{
throw new RuntimeException("不要试图使用反射破坏");
}
}
(6)如何绕过?
可以通过反编译获取flag参数,强行修改其为false
假设我们找到了这个变量
public class Test {
public static void main(String[] args) throws Exception {
Field flag = LazyMan.class.getDeclaredField("flag"); //假设知道该变量
flag.setAccessible(true);//无视权限
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true); //无视了私有的构造器
LazyMan lazyMan2 = constructor.newInstance();
flag.set(lazyMan2,false); //把第一个对象的值再改回来
LazyMan lazyMan1 = constructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
}
3.5 反射不能破坏枚举类型
我们看一下newInstance()
方法的源码:
如果类的类型为枚举(enum),则会报错 :不能使用反射来创建枚举类的对象
先看一下它的构造方法:
是私有的无参构造方法
我们尝试使用反射创建对象:
public class Test {
public static void main(String[] args) throws Exception {
EnumSingle enumSingle1 = EnumSingle.INSTANCE; //正常获取对象
//反射
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle enumSingle2 = declaredConstructor.newInstance();
System.out.println(enumSingle1);
System.out.println(enumSingle2);
}
}
出现报错,但是并不是我们想要的报错:没有这样的构造方法
说明我们看到的源码中的构造方法是错误的
换一种方法使用 javap 反编译一下class文件:
可以看到其中的构造方法也是无参的
再换一种方法,用 jad 工具反编译:
可以发现是带有两个参数的构造方法
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
更改一下参数,获得了我们想要的报错:
3.6 单例模式总结
单例模式的特性就是构造器私有
- 饿汉式单例模式
- 懒汉式单例模式(DCL懒汉式)(双重检测锁模式)
- 静态内部类
以上三种方法都可以使用反射来破坏
- enum 枚举类型 (无法被反射破坏)
四、工厂模式
4.1 作用
实现了创建者和调用者的分离
详细分类:
简单工厂模式
用来生产同一等级结构中的任意产品(对于增加新的产品,需要扩展已有代码)
工厂方法模式
用来生产同一等级结构中的固定产品(支持增加任意产品)
抽象工厂模式(与 其他两个还是有区别的)
围绕一个超级大厂创建其他工厂,该超级工厂又称为其他厂的厂
符合的OOP七大原则:
- 开闭原则
- 依赖倒转原则
- 迪米特原则
核心本质:
- 实例化对象不使用new,用工厂方法代替
- 将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类进行解耦
4.2 代码实现(简单工厂模式)
//Car.interface
public interface Car {
void name();
}
//WuLing.java
public class WuLing implements Car{
@Override
public void name() {
System.out.println("五菱宏光!");
}
}
//Tesla.java
public class Tesla implements Car{
@Override
public void name() {
System.out.println("特斯拉!");
}
}
//CarFactory.java
/**
* 简单工厂模式(静态工厂模式)
* 弊端:增加一个新的产品,如果你不修改代码,做不到
* 不满足开闭原则(对修改关闭)
*/
public class CarFactory {
public static Car getCar(String car){
if(car.equals("五菱宏光")){
return new WuLing();
}
else if(car.equals("特斯拉")){
return new Tesla();
}
else{
return null;
}
}
}
//Test.java
//测试类
public class Test {
public static void main(String[] args) {
//原先需要知道接口和所有的实现类
// Car car1 = new WuLing();
// Car car2 = new Tesla();
Car car1 = CarFactory.getCar("五菱宏光"); //现在不需要知道实现细节
Car car2 = CarFactory.getCar("特斯拉");
car1.name();
car2.name();
}
}
总结:
如果需要新加入一些汽车,就需要修改CarFactory的内容(if-else 结构),if-else 结构的维护性差,可读性差
但是对于固定和不经常修改的系统来说,还是很好的
4.3 代码实现(工厂方法模式)
在简单工厂模式的基础上,增加了工厂的接口
//CarFactory.interface
//工厂方法模式
public interface CarFactory {
Car getCar();
}
//WuLingFactory.java
public class WuLingFactory implements CarFactory{
@Override
public Car getCar() {
return new WuLing();
}
}
//测试类
//虽然满足了开闭原则,但是更麻烦了
public class Test {
public static void main(String[] args) {
Car car1 = new WuLingFactory().getCar();
car1.name();
}
}
总结:
Car接口用来创建产品对象,CarFactory接口用来优化 if-else 结构,使可读性增强,维护性增强
缺点:
对于不断增加的产品类,就需要不断创建它的实例化对象,这样实例化对象就会越来越多,造成类爆炸
五、抽象工厂模式
5.1 简介
定义:
抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口,无需指定它们具体的类
适用场景:
1、客户端(应用层)不依赖于产品类实例如何被创建、实现等细节
2、强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量的重复代码
3、提供一个产品类的库,所有的产品以同样的接口出现,从而使得客户端不依赖于具体的实现
优点:
1、具体产品在应用层的代码隔离,无需关心实现的细节
2、将一个系列的产品统一到一起创建
缺点:
1、规定了所有可能被创建的产品集合,产品族中扩展新的产品困难
2、增加了系统的抽象性和理解难度
5.2 代码实现
产品的实现和工厂方法模式是一样的,这里列举一下抽象工厂的构造:
//IProductFactory.interface
//抽象产品工厂
public interface IProductFactory {
//生产手机
IphoneProduct iphoneProduct();
//生产路由器
IrouterProduct irouterProduct();
}
//HuaweiFactory.java
public class HuaweiFactory implements IProductFactory{
@Override
public IphoneProduct iphoneProduct() { //返回手机产品实例
return new HuaweiPhone();
}
@Override
public IrouterProduct irouterProduct() { //返回路由器产品实例
return new HuaweiRouter();
}
}
//XiaomiFactory.java
public class XiaomiFactory implements IProductFactory{
@Override
public IphoneProduct iphoneProduct() { //返回手机产品实例
return new XiaomiPhone();
}
@Override
public IrouterProduct irouterProduct() { //返回路由器产品实例
return new XiaomiRouter();
}
}
总结:
第一眼看上去还是和工厂方法模式很像的,因为都有两个接口,一个实现了产品类,一个实现了工厂类
两个模式最大的区别在于,工厂方法模式作用于一个品牌的产品,每个产品都需要一个工厂类
而抽象工厂模式,将所有的产品都放到了一个品牌的工厂类中缩减了工厂实现子类的数量
六、建造者模式
建造者模式也属于创建型模式,它提供了一种创建对象的最佳方式
定义:
将一个复杂对象的创建和与他的表示分离,使得同样的创建过程可以创建不同的表示
主要作用:
在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象
用户只要给出指定复杂对象的类型和内容,建造者模式负责按顺序创建复杂对象(把内部的建造过程和细节隐藏起来)
6.1 代码实现(常规方法)
导演类(Director)在Builder模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况需要简化系统结构,可以把Director和抽象建造者进行结合
//Product.java
//产品类
public class Product {
//产品有三个功能
private String buildA;
private String buildB;
private String buildC;
private String buildD;
//以下的Getter、Setter、toString 方法都省略了
//抽象的建造者:方法
public abstract class Builder {
abstract void buildA();
abstract void buildB();
abstract void buildC();
abstract void buildD();
//完工,得到产品
abstract Product getProduct();
}
//具体的建造者
public class Worker extends Builder {
//具体的建造者 建造产品并返回
public Worker() {
this.product = new Product();
}
private Product product;
@Override
void buildA() {
product.setBuildA("A");
System.out.println("A");
}
@Override
Product getProduct() {
return product;
}
//一些具体的实现省略了
}
//指挥:用来指挥构建工程,工程如何创建由他决定
//相当于对建造者的工作有了一个指示
public class Director {
//指挥工人按照顺序建房子
public Product build(Builder builder){
builder.buildA();
builder.buildB();
builder.buildC();
builder.buildD();
return builder.getProduct();
}
}
//测试类
public class Test {
public static void main(String[] args) {
//指挥
Director director =new Director();
//工人把产品指挥权交给指挥,指挥最后返回完整的产品
Product product = director.build(new Worker());
System.out.println(product.toString());
}
}
6.2 代码实现(更灵活的方法)
通过静态内部类方式实现零件无序装配构造,这种方式使用更加灵活,更符合定义。内部有复杂对象的默认实现,使用时可以根据用户需求自定义更改内容,并且无需改变具体的构造方法,就可以生产出不同的复杂产品
//产品
public class Product {
//默认套餐
private String BuildA ="汉堡";
private String BuildB ="可乐";
private String BuildC ="薯条";
private String BuildD ="甜点";
//参数的 Getter、Setter、toString 方法具体实现省略了
}
//抽象的建造者
public abstract class Builder {
//用户可以自定义套餐(msg)
//注意返回的类型为Builder本身
abstract Builder buildA(String msg); //汉堡
abstract Builder buildB(String msg); //可乐
abstract Builder buildC(String msg); //薯条
abstract Builder buildD(String msg); //甜点
abstract Product getProduct();
}
public class Worker extends Builder{
//工人构建产品
private Product product;
public Worker() {
this.product = new Product();
}
@Override
Builder buildA(String msg) {
product.setBuildA(msg);
return this;
}
@Override
Product getProduct() {
return product;
}
//一些方法的具体实现省略
}
//用户
public class Test {
public static void main(String[] args) {
Worker worker = new Worker();
//链式编程:在原来的基础上可以自由组合了,如果不组合也有默认的套餐
Product product = worker.buildA("大汉堡").getProduct();
System.out.println(product.toString());
}
}
这里之所以可以使用链式编程,细节在于抽象Builder类中的方法的返回类型为Builder,那么也就可以自定义其中的参数了
总结:
如果将抽象工厂模式看成汽车的配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车的组装工厂,通过对部件的组装返回一个完整的汽车
这里我最开始还有一个疑问:为什么要使用抽象类?
我自己尝试了接口之后,发现也可以运行,抽象类和接口比较类似,都有自己的特点
七、原型模式
克隆原来的
7.1 代码实现(浅克隆)
/**
* 实现一个接口 Cloneable
* 重写一个方法 clone()
*/
//视频的原型
public class Video implements Cloneable{
private String name;
private Date createTime;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public Video() {
}
public Video(String name, Date createTime) {
this.name = name;
this.createTime = createTime;
}
//以下Getter、Setter、toSting 方法省略
}
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
//原型对象
Date date = new Date();
Video v1 = new Video("siii0",date);
System.out.println("v1=>"+v1);
System.out.println("v1=>hash:"+v1.hashCode());
//克隆
//克隆出来的对象和原来是一样的
Video v2 = (Video) v1.clone();
System.out.println("v2=>"+v2);
System.out.println("v2=>hash:"+v2.hashCode());
}
}
不过这种克隆方法属于浅克隆:
假如原型Video的date改变了,克隆出来的Video也会随着改变
7.2 代码实现(深克隆)
可以通过改写clone()方法来实现深克隆
也可以通过序列化、反序列化来实现深克隆
//改写的clone()方法
@Override
protected Object clone() throws CloneNotSupportedException {
//实现深克隆
Object obj = super.clone();
Video v = (Video) obj;
//将这个对象的属性也进行克隆
v.createTime = (Date) this.createTime.clone();
return obj;
}
这样就实现了深克隆:
总结:
Spring Bean :单例模式、原型模式
原型模式也会和工厂模式结合使用,提高了效率:
原型模式 + 工厂模式 ==> new <==> 原型模式
原型模式主要是实现了:
接口 Cloneable
方法 clone()
7.3 代码实现(建造者模式+原型模式)
首先,建造者模式存在产品类和建造者类,这两个类都需要实现克隆(深克隆)
//产品类实现了克隆接口
//其他具体方法实现省略
public class Product implements Cloneable{
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
//工人类实现了Buile接口和克隆接口
public class Worker implements Builder,Cloneable{
public Worker() {
this.product = new Product();
}
private Product product;
//实现克隆,同时需要克隆产品,实现深克隆,否则就是浅克隆
//返回一个完全克隆的Woker类
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj = super.clone();
Worker worker = (Worker) obj; //强制类型转换
worker.product = (Product) this.product.clone(); //产品类的克隆(深克隆)
return obj;
}
}
//测试类
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Worker worker = new Worker();
Product product = worker.buildB("BB").buildD("DD").getProduct();
Worker worker1 = (Worker) worker.clone();
Product product1 = worker1.buildB("BBB").buildD("DDD").getProduct();
}
- 本文链接:http://siii0.github.io/java%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B8%80%EF%BC%88%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F%EF%BC%89/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。