设计模式汇总
1.单例模式
模式意图: 保证一个类只有一个实例,并且提供一个全局访问点
应用场景:
- 需要严格的控制全局变量时.
- 重量级的对象,例如线程池,数据库连接对象/不需要多个实例对象的工具类.
设计模式:
-
懒汉式:
package 单例模式; public class 懒汉式 { public static void main(String[] args) { } } /** * 首先,会出现线程安全问题 * 解决方法:给getInstance方法加锁,但这样效率很低,我们可以在if里面双重加锁来优化 * */ class LazySingleton{ private volatile static LazySingleton instance; private LazySingleton(){ } public synchronized static LazySingleton getInstance(){ if(instance==null){ synchronized (LazySingleton.class) { if(instance==null) instance = new LazySingleton(); //1. 开辟空间 //2. 给instance赋值 //3. 初始化空间 } } //当线程一进入//2是线程2可能会直接跳到这里然后返回一个没有初始化好的对象,所以我们需要给对象加上volatile,来防止指令重排 return instance; } }
-
饿汉式:
只有在真正主动使用对应的类时,才会触发初始化(当前类是启动类,即main函数的所在类,直接进行new操作,访问静态属性,访问静态方法,用反射访问类,初始化一个类的子类等)
类加载的初始化阶段就完成了实例的初始化,本质上是借助jvm类加载机制,保证市里的唯一性(初始化过程只会执行一次) 以及线安全(JVM一同步的形式来完成类加载的整个过程).
类加载过程:
- 加载二进制文件到内存中,生成对应的class数据结构
- 链接: a.验证,b 准备(给类的静态成员变量赋默认值) c. 解析
- 初始化:给类的静态变量赋初值
class HungrySingleton{
private static HungrySingleton instance =new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance() {
return instance;
}
}
-
内部类:
class InnerClassSingleton{ private static class SingletonHolder{ private static InnerClassSingleton instance=new InnerClassSingleton(); } private InnerClassSingleton() { } public static InnerClassSingleton getInstance(){ return SingletonHolder.instance; } }
相比于饿汉式的优点:只有在调用getInstance()时才会初始化对象
但是上述的方法都不能阻止反射,例如下面的代码:
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<LazySingleton> declaredConstructor = LazySingleton.class.getDeclaredConstructor();//获得构造器
declaredConstructor.setAccessible(true);//绕过private修饰符
LazySingleton lazySingleton = declaredConstructor.newInstance();//创建新实例
System.out.println(lazySingleton == LazySingleton.getInstance());//判断
}
最后的结果是false
我们可以跟进反射的newInstance()方法,发现如果构造的对象类型是枚举,就会直接抛出异常,于是就有了下一个单例方法:
-
枚举
public class 枚举 { public static void main(String[] args) { EnumSingleton instance = EnumSingleton.INSTANCE; instance.print(); } } enum EnumSingleton{ INSTANCE; public void print(){ System.out.println("xxxx"); } }
2.工厂模式
模式定义: 定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method是的一个类的实例化延迟到子类.
具体流程:抽象出一系列对象的共同点并制作为接口(这个就是工厂),将这些对象分别实现对应的接口(具体产品),使用时只需要将工厂构建出来即可,这样不同的对象(但具有类似行为)就可以通过相同的代码来操作.
例子(设计畜牧场):
有很多种类的畜牧场,如养马场用于养马,养牛场用于养牛,
这里我们很容易京能吧对象归为两类:农场和动物;所以我们就定义两个接口:Animal 和AnimalFarm ;
类图:
接口代码:
interface Animal { public void show();}
interface AnimalFarm { public Animal newAnimal();}
测试代码:
public static void main(String[] args) {
try {
Animal a;
AnimalFarm af;
af = (AnimalFarm) ...//根据具体情况编写
a = af.newAnimal();
a.show();
} catch (Exception e) {
System.out.println(e.getMessage());
}
优点:
- 不用管要使用的对象的确切类型;
- 可以很方便的为框架扩展
- 符合单一职责原则
- 符合开闭原则
- 将具体产品和创建者解耦合
3.抽象工厂模式
提供一个创建一系列相关或者互相依赖对象的接口,而无需指定他们具体的类
说白了就是多个工厂模式组合在一起,就形成了"抽象工厂模式"举个类图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ZkJ8mvW-1624664280322)(C:\Users\张博文\AppData\Roaming\Typora\typora-user-images\image-20210622033328166.png)]
应用场景:程序需要处理不同系列的相关产品,但是又不希望它依赖与这些产品的具体类,可以使用抽象工厂.
优点:
- 可以确定你从工厂里得到的产品是彼此兼容的
- 可以避免具体产品和客户端代码之间的紧密耦合
- 符合单一职责原则
- 符合开闭原则
JDK源码中的例子:JDBC中Connection其实是个接口…
4. 建造者模式
当我们要初始化一个庞大的资源类(这个类的内部有大量的属性和字段需要初始化)时,使用传统的构造器会显得十分不明朗,这个时候我们就可以采用建造者模式,
类图:
建造者模式有两种写法(不过现在都倾向于写第二种):
第一种的流程:
- 书写一个接口,里面定义了每一个需要初始化的属性的set方法,和build()方法
- 书写这个接口的实现类,这个类要有上边需要初始化的属性,需要注意的是,build方法里面应该有属性监测,并应该有对应的异常处理.
- 为了简化代码和部分属性的初始化可能有顺序限制,我们最好再包装一个director类来指引builder类的初始化(所以在初始化类的时候还是new (一大堆参数));
反思了一下,这么大费周折的好处就是,每一种参数的初始化过程会变得比较清晰.毕竟之前的就是都在一个大函数里,没有良好的注释是看得很麻烦的.(窗体的初始化以及构造就是)
简单代码:
package 建造者模式;import java.util.Arrays;public class 方法一 { public static void main(String[] args) { Director director=new Director(new Builder()); Figure figure = director.makeFigure(new int[]{3, 4, 5}, 3, "三角形"); System.out.println(figure); }}interface FigureBuilder{ void setEdges(int[] edges); void setNum(int num); void setName(String name); void cumS(); void cumC(); Figure build();}class Builder implements FigureBuilder{ private int[] edges; private int num; private String name; private double square; private int circumference; @Override public void setEdges(int[] edges) { this.edges=edges; } @Override public void setNum(int num) { this.num = num; } @Override public void setName(String name) { this.name=name; } @Override public void cumS() { //算不出来.. } @Override public void cumC() { for (int i:this.edges) { this.circumference+=i; } } @Override public Figure build() { //做检查 return new Figure(edges,num,name,square,circumference); }}class Director{ FigureBuilder builder; public Director(FigureBuilder builder){ this.builder=builder; } public Figure makeFigure(int[] edges, int num, String name){ builder.setEdges(edges); builder.setName(name); builder.setNum(num); builder.cumC(); builder.cumS(); return builder.build(); }}class Figure{ private int[] edges; private int num; public String name; private double square; private int circumference; public Figure(){ throw new RuntimeException("不能初始化!"); } public Figure(int[] edges, int num, String name, double square, int circumference) { this.edges = edges; this.num = num; this.name = name; this.square = square; this.circumference = circumference; } @Override public String toString() { return "Figure{" + "edges=" + Arrays.toString(edges) + ", num=" + num + ", name='" + name + '\'' + ", square=" + square + ", circumference=" + circumference + '}'; }}
第二种的流程:
- 书写一个静态 final 内部类 ,在里边书写所有需要初始化参数的set方法,
- 书写build方法(),build方法里面应该有属性监测,并应该有对应的异常处理.
- 这种方法一般不会再写director类来引导builder,而是通过程序员自己的约定来实现资源类的构建
简易代码:
package 建造者模式;import java.util.Arrays;public class 方法2 { public static void main(String[] args) { Figure2 figure2 = new Figure2.Builder() .setEdges(new int[]{3, 4, 5}) .setName("三角形") .setNum(3) .cumC() .cumS() .build(); System.out.println(figure2); }}class Figure2{ private int[] edges; private int num; public String name; private double square; private int circumference; public Figure2(){ throw new RuntimeException("不能初始化!"); } public Figure2(int[] edges, int num, String name, double square, int circumference) { this.edges = edges; this.num = num; this.name = name; this.square = square; this.circumference = circumference; } @Override public String toString() { return "Figure2{" + "edges=" + Arrays.toString(edges) + ", num=" + num + ", name='" + name + '\'' + ", square=" + square + ", circumference=" + circumference + '}'; } public static class Builder { private int[] edges; private int num; public String name; private double square; private int circumference; public Builder() { } public Builder setEdges(int[] edges) { this.edges = edges; return this; } public Builder setNum(int num) { this.num = num; return this; } public Builder setName(String name) { this.name = name; return this; } public Builder cumC() { for (int i : this.edges) { this.circumference += i; } return this; } public Builder cumS() { //算不出来.. return this; } public Figure2 build() { return new Figure2(edges, num, name, square, circumference); } }}
5.原型模式
所谓原型模式,就是把一个对象当做原型,需要的是时候以这个原型快速的拷贝出来一系列的副本供使用,原型模式的使用很灵活,这要归功于浅拷贝和深拷贝,我们先研究一下他们两者还有直接赋值之间的区别:
-
直接赋值
如果等号双方都是简单类型(int,String之类的),那么他们首先会指向同一块常量池,当有一方被修改时,才会开辟新的空间
但如果双方都是对象时,会直接把对方的引用复制过来,这会导致两个对象值相同一块空间,修改一个对象中的任意属性都会导致另一个对象中的对应属性发生改变
-
浅拷贝
这个是针对非简单类型的定义,通过实现Cloneable接口(重写clone方法)返回父级的clone方法即可(该方法由native关键字修饰)使用该方法来复制就会使两个对象里的基础类型相互隔离,但是两个对象里的非基础类型属性依然指向同一块地址,
简单代码:
package 原型模式;public class 浅拷贝 { public static void main(String[] args) { School school=new School(new Student("zhangsan",1),"school1"); try { School clone = (School)school.clone(); System.out.println(clone); System.out.println(school); school.student.name="lisi"; System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); System.out.println(clone); System.out.println(school); } catch (CloneNotSupportedException e) { e.printStackTrace(); } }}class Student{ String name; int id; public Student(String name, int id) { this.name = name; this.id = id; } @Override public String toString() { return "["+this.hashCode()+" ]Student{" + "name='" + name + '\'' + ", id=" + id + '}'; }}class School implements Cloneable{ Student student; String name; public School(Student student, String name) { this.student = student; this.name = name; } @Override public String toString() { return "["+this.hashCode()+" ]School{" + "student=" + student + ", name='" + name + '\'' + '}'; } @Override public Object clone() throws CloneNotSupportedException { return super.clone(); }}
-
相对于浅拷贝改进了唯一一点不足,即使是非基础类型,我们通过深拷贝也可以保证两者的内存空间不同(深拷贝的实现方法不唯一,还可以考虑使用流)
代码:
package 原型模式;public class 深拷贝 { public static void main(String[] args) { School2 school=new School2(new Student2("zhangsan",1),"school1"); try { School2 clone = (School2)school.clone(); System.out.println(clone); System.out.println(school); school.student.name="lisi"; System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); System.out.println(clone); System.out.println(school); } catch (CloneNotSupportedException e) { e.printStackTrace(); } }}class Student2 implements Cloneable{ String name; int id; public Student2(String name, int id) { this.name = name; this.id = id; } @Override public String toString() { return "["+this.hashCode()+" ]Student2{" + "name='" + name + '\'' + ", id=" + id + '}'; } @Override public Object clone() throws CloneNotSupportedException { return super.clone(); }}class School2 implements Cloneable{ Student2 student; String name; public School2(Student2 student, String name) { this.student = student; this.name = name; } @Override public String toString() { return "["+this.hashCode()+" ]School2{" + "student=" + student + ", name='" + name + '\'' + '}'; } @Override public Object clone() throws CloneNotSupportedException { School2 school2 = (School2) super.clone(); school2.student= (Student2) student.clone(); return school2; }}
-
总结,抛开直接赋值不谈,深浅拷贝的唯一不同就是clone方法的书写,深拷贝的clone方法时递归式的,仅此而已…
通过深浅拷贝,我们的原型模式就可以在很多地方使用了,比如多个用户需要修改同一个文本(浅拷贝),单纯的复制大量对象,但是new太过于耗费资源(深拷贝)
--------------------------
6.适配器模式
还是狂神的视频看着舒服啊!!!,不用到处查资料,爽到,适配器模式很简单,两个类之间没有直接的联系(这个是根据用户需求画出来的类图决定的,没事不要乱YY两个类之间的关系,胡乱的增加耦合在以后的开发中会很麻烦)但是A类又想调用B类的功能,这个时候就可以通过适配器模式来整理这个关系.
适配器模式有两种设计方法: 类适配器和对象适配器,将会在下面的代码中做详细的演示.
举个例子: 电脑要通过网线来上网,但是,电脑没有网线的接口,这个时候我们就需要适配器来作为"桥梁"来让电脑和网线连接.
简单示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fTXKz96O-1624664280324)(C:\Users\张博文\AppData\Roaming\Typora\typora-user-images\image-20210623073734881.png)]
代码:
类适配器模式:
package 适配器模式;public class 类适配器 { public static void main(String[] args) { Cumputer cumputer = new Cumputer(); cumputer.getNet(new NetAdapter()); }}class Cumputer{ void getNet(NetToUSB adapter){ adapter.handleRequire(); }}class NetAdapter extends NWCable implements NetToUSB{ @Override public void handleRequire() { super.net(); }}class NWCable{ void net(){ System.out.println("已经连接上网线"); }}interface NetToUSB{ void handleRequire();}
对象适配器模式:
package 适配器模式;public class 对象适配器 { public static void main(String[] args) { Cumputer cumputer = new Cumputer(); cumputer.getNet(new NetAdapter2(new NWCable())); }}class NetAdapter2 implements NetToUSB{ NWCable cable; public NetAdapter2(NWCable cable){ this.cable=cable; } @Override public void handleRequire() { cable.net(); }}class NWCable{ void net(){ System.out.println("已经连接上网线"); }}interface NetToUSB{ void handleRequire();}
可以很明显的观察出来: 类适配器是非常不友好的,因为他使用了继承,而JAVA是单继承的,所以这就会导致它比对象适配器更局限,但是对象适配器写起来可能更麻烦一点.
这么做会使得当电脑有多种链接网线的方法时,拓展起来很方便,不用修改Cumputer类和NWcable类.(符合开闭原则)
7.桥接模式
桥接模式又被称作柄体模式或者接口模式,当我们遇到大量的继承关系时我们可以通过桥接模式来简化我们需要创建的类,例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ThLaUrH2-1624664280325)(C:\Users\张博文\AppData\Roaming\Typora\typora-user-images\image-20210624075849304.png)]
在普通的设计思想中,要实现这个关系需要构建大量的类,并且都是继承的关系,这会使我们要构建大量的类,但这明显是不划算的,因为我们通过观察发现,这么多的子类其实都只有两部分组合(电脑类型和品牌)所以我们构建两个抽象类/接口分别为Computer和Brand,使用组合的方式将两者联系起来,定义三种不同的电脑和三种不同的品牌分别实现他们,这样就实现了上图的关系结构
代码:
package 桥接模式;public class 桥接模式 { public static void main(String[] args) { Computer computer =new Product1(new Brand1()); Computer computer1 =new Product2(new Brand2()); computer.getInfo(); computer1.getInfo(); }}abstract class Brand{ String brandName; void setBrandName(String name){ this.brandName=name; }}class Brand1 extends Brand{ public Brand1(){ setBrandName("Brand1"); }}class Brand2 extends Brand{ public Brand2(){ setBrandName("Brand2"); }}abstract class Computer { protected Brand brand; String productName; public Computer(Brand brand){ this.brand=brand; } public void setProductName(String name){ productName=name; } public void getInfo(){ System.out.println(brand.brandName+":"+productName); }}class Product1 extends Computer { public Product1(Brand brand) { super(brand); setProductName("Product1"); }}class Product2 extends Computer { public Product2(Brand brand) { super(brand); setProductName("Product2"); }}
好处:
- 桥接模式偶尔类似于多继承方案,但是多继承方案违反了累的单一职责原色,复用性比较差,类的个数也很多,桥接模式势必集成模式更好地结局方案,极大的减少了子类的个数,从而降低了管理和维护的成本
- 桥接模式提高了系统的可拓展性,在两个变化的维度中任意扩展一个维度,都不需要修改原有系统,符合开闭原则,就像一座桥,可以吧两个变化的维度链接起来
劣势:
- 桥接模式的引入会正价系统的理解与设计难度.由于聚合关联关系简历在抽象层,要求开发者针对抽象进行设计与编程
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围有一定的局限性
8.代理模式
静态代理
角色分析:
- 抽象角色:一般会使用接口或者抽象类来解决
- 真实角色:被代理的角色
- 代理角色:代理真实角色,代理真实角色后一般我们会做一些附属操作(不然就不用了代理了)
- 客户:访问代理的人
代理模式的好处:
-
可以是真实的角色的操作更加纯粹,不用去关注一些公共的业务
-
公共的业务交给代理,实现类业务的分工
-
公共业务发生拓展的方便集中管理
-
可以在不修改原有代码的基础之下,可以很轻松的添加拓展
缺点:每一个真实角色就会产生一个代理角色,开发效率会变低(解决方法就是动态代理)
代码:
package 代理模式;public class 静态代理 { public static void main(String[] args) { Proxy proxy = new Proxy(new Host("zhangsan")); proxy.seeHome(); proxy.rent(); proxy.fare(); }}interface Rent{ void rent();}class Host implements Rent{ String name; public Host(String name){ this.name=name; } @Override public void rent() { System.out.println(name+"的房子租出去了"); }}class Proxy{ Host host; public Proxy(Host host) { this.host = host; } void seeHome(){ System.out.println("中介带你看房子"); } void rent(){ host.rent(); } void fare(){ System.out.println("收取中介费"); }}
动态代理
- 动态代理和静态代理的角色一样
- 动态代理的代理类是动态生成的,不是我们直接写好的
- 动态代理分为两大类:基于接口的动态代理和基于类的动态代理
- 基于接口:JDK动态代理[我们在这里使用]
- 基于类: cglib
- java字节码实现:Javasist
我们需要了解两个东西:Proxy(类)->代理,invocationHandle(接口)->调用处理程序.
详细的解释在代码里了
package 代理模式;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class 动态代理 { public static void main(String[] args) { //真实角色 Host host=new Host("zhangSan"); //代理角色:现在没有 ProxyInvocationHandler pih=new ProxyInvocationHandler(); //通过调用程序处理角色来处理我们要调用的接口对象 pih.setRent(host); Rent proxy=(Rent) pih.getProxy(); proxy.rent(); }}//使用这个类动态生成代理class ProxyInvocationHandler implements InvocationHandler { //被代理的接口 private Rent rent; public void setRent(Rent rent) { this.rent = rent; } //生成得到代理对象 public Object getProxy(){ return Proxy.newProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterfaces(),this); } /** * * @param proxy * @param method 要执行额方法 * @param args 参数列表 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(rent,args);//第一个参数是指哪一个对象执行该方法,第二个参数是参数列表,想要拓展业务就在这个函数里面执行 }}
9. 装饰器模式
装饰期末实和桥接模式长得很像,个人感觉区别他们两个的应用场景的地方在于,附加的东西是否可以不断的附加,举个例子:
每本书只有一个出版商,但是一道主菜可以有很多种副菜,前者适合桥接模式,后者适合装饰器模式
其实和桥接模式一样,装饰器模式也是为了避免出现类爆炸的情况而诞生的,但是和装饰器模式不同的是这个前缀可以不停地叠加,所以我们就需要对原本的桥接模式进行一些改进,让他来适应这个类似递归的操作,简单的说就是原本的两个抽象类之间建立继承关系,这样的话我们每构建出一个新对象这个新的对象就会生成一个新的可以附加的前缀,这样就可以形成类似"装饰"的效果–也就叫装饰器模式了…
下面是类图:
简单代码:
package 装饰器模式;import com.sun.tools.javac.Main;public class 装饰器模式 { public static void main(String[] args) { //只有主食 MainFood myfood=new MainFood1(); myfood.show(); System.out.println("======================="); //加了副食1 myfood=new Garnish1(myfood); myfood.show(); System.out.println("======================="); //再次加了副食1 myfood=new Garnish1(myfood); myfood.show(); }}//被修饰的类,判别那个类被修饰其实只用看那个对象是"词根"就行,因为是个很简单的例子,所以每个具体的实现类(子类)没有不同的地方,就没有书写abstracct 方法了,但是记得项目中应该是会有的abstract class MainFood{ String name; int price; void show(){ System.out.println(name+":"+price); }}//主食的实现类class MainFood1 extends MainFood{ public MainFood1() { name="MainFood1"; price=100; }}//装饰类,只能依托于被修饰的类存在,不能单独存在(这个其实在构造函数里改一下就行.)//同上,没有抽象方法abstract class Garnish extends MainFood{ MainFood mainFood; public Garnish(MainFood mainFood) { this.mainFood = mainFood; } public Garnish1(MainFood mainFood){ super(mainFood); price=10+ mainFood.price; name="garnish1+"+mainFood.name; }}