1. 代理模式
1.1 介绍
由于某些原因需要给某对象提供一个代理以控制对该对象的访问,这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介
Java中的代理按照代理类生成的时机不同分为静态代理和动态代理,静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成,动态代理悠悠JDK代理和CGLib代理两种
结构:
代理(Proxy)模式分为三种角色:
抽象主题(subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法
真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象
代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能
类图:
cdglib:
jdk:
static:
1.1.1 静态代理
直接写死
public class ProxyPoint implements SellTickets {
private TrainStation trainStation = new TrainStation();
public void sell(){
System.out.println("代理点收取一些服务费用");
trainStation.sell();
}
}
1.1.2 动态代理
1.1.2.1 JDK动态代理:
代理类($Proxy0)实现了SellTickets,这也就印证了我们之前说的真实类和代理类实现同样的接口
代理类($Proxy0)将我们提供的匿名内部类对象传递给了父类
1.12.2 CGLIB动态代理:
导入依赖cglib
- CGLIB是一个功能强大、高性能的代码生产包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很多补充
- CGLIB是第三方提供的包,所以需要引入jar包的坐标
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
1.2 三种代理的对比
- jdk代理和CGLib代理:
使用CGLib代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高,唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类
再JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理,所以如果有接口使用JDK动态代理,如果没有接口使用CGLib代理
- 动态代理和静态代理:
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理
(InvocationHandler.invoke),这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法,增加了代码维护的复杂度,而动态代理不会出现该问题
优缺点:
优点:
代理模式在客户端与目标对象之间起到一个中介和保护目标对象的作用
代理对象可以扩展目标对象的功能
代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度
缺点:增加了系统的复杂度
1.3 使用场景
远程代理:将网络通信暴露给本地服务的一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关系通信部分的细节
防火墙代理:将浏览器设置成代理功能后,防火墙将你的浏览器请求转发给互联网,当互联网返回响应时,代理服务器再把它转给你的浏览器
保护代理:控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限
2. 适配器模式
2.1 介绍
定义:
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少写
结构:
目标接口:当前系统业务所期待的接口,他可以是抽象类或接口
适配者类:它是被访问和适配的现存组件库中的组件接口
适配器类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者
2.1.1 类适配器模式:
类适配器类,通过实现接口和继承需要适配的对象,从而充当一个适配的对象
类图:
通过一个例子来说明
public class SDAdapterTF extends TFCardImpl implements SDCard{
类适配器违背了合成复用原则,类适配器是客户类有一个接口规范的情况下使用,反之不可用
2.1.2 对象适配器模式:
实现方式:对象适配器模式可采用现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口
类图:
2.2 应用场景
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同
2.3 JDK源码解析
Reader(字符流)、InputStream(字节流)的适配器使用的是InputStreamReader
InputStreamReader继承来自java.io包中的Reader,对他中的抽象的未实现方法给出实现
从上图,可了解到StreamDecoder将InpuStream转换成了Reader,通过方法forInputStreamReader()
,而InputStreamReader是对StreamDecoder的封装调用,封装到了构造函数中
Reader reader = new InputStreamReader(new InputStream() {
public int read() throws IOException {
return 0;
}
});
3. 装饰者模式
3.1 介绍
定义:
在不改变现有对象结构的情况下,动态地给对象增加一些职责(即增加其额外功能)的模式
结构:
装饰者模式中的角色:
抽象构件角色:定义一个抽象接口以规范准备接收附加责任的对象
具体构件角色:实现抽象构件,通过装饰角色为其添加一些职责
抽象装饰角色:继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能
具体装饰角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的职责
类图:
优点:
装饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果,装饰者模式比继承更具有良好的扩展性,完美的遵循开闭原则,集成式静态的附加责任,装饰者则是动态的附加责任
装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能
3.2 使用场景
- 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
不能采用继承的情况主要有两大类:
- 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长
- 第二类是因为类定义不能继承(如final类)
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
- 当对象的功能要求可以动态地添加,也可以再动态的撤销时
3.3 JDK源码
Writer和BufferWriter、InputStreamWriter用到的模型\
装饰者巧妙之处在于聚合(具体的构件类成为成员变量)了构件类,又继承了构建类
3.4 代理和装饰者的区别
3.4.1 静态代理和装饰者模式的区别:
异同点:
相同点:
- 都要实现与目标类相同的业务接口
- 在两个类中都要声明目标对象
- 都可以在不修改目标类的前提下增强目标方法
不同点:
目的不同
- 装饰者是为了增强目标对象
静态代理是为了保护和隐藏目标对象
- 获取目标对象构建的地方不同
- 装饰者是由外界传递进来,可以通过构造方法传递
- 静态代理是在代理类内部创建,以此来隐藏目标对象
4. 桥接模式
4.1 介绍
定义:
将抽象和实现分离,使它们可以独立变化,它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度
结构:
桥接模式包含以下主要角色:
抽象化角色:定义抽象类,并包含一个对实现化对象的引用
扩展抽象化角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用来实现化角色中的业务方法
实现化角色:定义实现化角色的接口,供扩展抽象化角色调用
具体实现化角色:给出实现化角色接口的具体实现
类图:
优点:
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统
如:如果现在还有一种视频文件类型wmv,我们只需要再定义一个类实现VideoFile接口即可,其他类不需要发生变化
- 实现细节对客户透明
4.2使用场景
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系
5. 外观模式
5.1 介绍
定义:
又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而这些子系统更加容易被访问的模式,该模式对外有一个统一接口,外部应用程序不用关系内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提供了程序的可维护性。
外观模式是“迪米特法则”的典型应用
结构:
外观(Facade)模式包含以下主要角色:
外观(Facade)角色:为多个子系统对外提供一个共同的接口
子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它
类图:
优缺点:
优点:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响到调用它的客户类
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易
缺点:
不符合开闭原则,修改很麻烦
5.2 使用场景
- 对分层结构系统构件是,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系
- 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问
- 当客户的与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性
6. 组合模式
6.1 介绍
定义:
又名部分整体模式,是用于把一组相似的对象当做一个单一的对象,组合模式依据树结构来组合对象,用来表示部分以及整体层次,这种类型的设计模式属于结构型模式,它创建了对象组的树形结构
结构:
抽象根节点(Component):定义系统各层次对象的公有方法和属性,可以预先定义一些默认行为和属性
树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构
叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位
类图:
示例图:
优点:
- 组合模式可以清晰地定义层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码
- 在组合模式中增加新的树枝节点和叶子结点都很方便,无须对现有类库进行任何修改,符合”开闭原则“
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单
6.2 使用场景
组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方,比如:文件目录提示,多级目录呈现等树形结构数据的操作
7. 享元模式
7.1 介绍
定义:
运用共享技术来有效地支持大量细纹度对象的复用,它通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率
结构:
享元(Flyweight)模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分
- 外部状态,指随环境改变而改变的不可以共享的部分,享元模式的实现要领就是区分应用中的这两种状态,并对外部状态外部化
享元模式的主要有以下角色:
- 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)
- 具体享元(Contrete Flyweight)角色:它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间,通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象
- 非享元(Unsharable Flyweight)角色:并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建
- 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,如果存在则提供给客户,如果不存在的话,则创建一个新的享元对象
类图:
优缺点:
优点:
- 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
- 享元模式中的外部状态相对独立,且不影响内部状态
缺点:
为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
7.2 使用场景
- 一个系统有大量相同或者相似的对象,避免内存的大量耗费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式
7.3 JDK源码解析
Integr类使用了享元模式
public static Integer valueOf(int var0) {
return var0 >= -128 && var0 <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[var0 + 128] : new Integer(var0);
}
static {
int var0 = 127;
String var1 = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
int var2;
if (var1 != null) {
try {
var2 = Integer.parseInt(var1);
var2 = Math.max(var2, 127);
var0 = Math.min(var2, 2147483518);
} catch (NumberFormatException var4) {
}
}
high = var0;
cache = new Integer[high - -128 + 1];
var2 = -128;
for(int var3 = 0; var3 < cache.length; ++var3) {
cache[var3] = new Integer(var2++);
}
源码中,-128-127的两个相同的Integer使用的是同一块内存,即共享了一个Integer创建的实例