结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象。
对于组合关系或聚合关系比继承关系耦合度低,满足”合成复用原则“,所以对象结构比类结构模式具有更大的灵活性。
结构模式分为以下7种:
代理模式,适配器模式,装饰者模式,桥接模式,外观模式,组合模式,享元模式
由于某些原因需要给某个对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中的代理按照代理类生成的时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。
代理模式包含的角色:
抽象主题:通过接口或抽象类表明真实主题和代理对象实现的业务方法。
真实主题:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象
代理类:提供与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问,控制或扩展真实主题的功能。
例子:火车站买票
要买火车票需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都要代售点,我们去代售点买票就方便很多了,这个例子显然就是典型的代理模式,火车站是目标对象,代售点是代理对象‘
抽象主题
- public interface SellTickets {
- void sell();
- }
真实主题
- public class TrainStation implements SellTickets {
- @Override
- public void sell() {
- System.out.println("火车站卖票!");
- }
- }
代理类
- public class ProxyPoint implements SellTickets {
- private TrainStation station = new TrainStation();
-
- @Override
- public void sell() {
- System.out.println("卖票前准备");
- station.sell();
- }
- }
直接访问代理类,也就是说代理类作为访问对象和目标对象的媒介,同时也对被代理类进行了增强
Java提供了一个动态代理类Proxy,Proxy并不是上述代理类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。
代理类工厂
- public class ProxyFactory {
- //声明目标对象
- private TrainStation trainStation = new TrainStation();
-
- public SellTickets getProxyObject() {
- //返回代理对象
- //参数:Classloader loader:类加载器,用于加载代理类
- //参数:Class<?>[] interfaces:代理类实现的接口的字节码对象
- //参数:InvocationHandler h:代理对象的调用处理程序
- SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
- trainStation.getClass().getClassLoader(),
- trainStation.getClass().getInterfaces(),
- new InvocationHandler() {
- //参数:proxy 代理对象,method 接口中的方法进行封装,args 调用方法的实际参数
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- //对目标方法的增强
- System.out.println("买票前的准备");
- //执行目标对象的方法
- method.invoke(trainStation, args);
- return null;
- }
- });
- return proxyObject;
- }
- }
底层帮我们生成一个继承Proxy类并实现了被代理类所实现的接口的代理类。
在构造器其中将InvocationHandler对象传入父类(Proxy类)
在静态代码块中通过反射的方式获取到被代理类实现的接口中的方法,和equals,toString,hashCode方法。并将它们传入代理类的Method类型的属性中。
然后在和接口类方法同名的方法中通过invoke()方法调用被代理类的同名方法。
测试
执行流程:
在测试类调用代理对象的sell()方法
根据多态的特性,执行的是代理类($Proxy0)中的sell()方法
代理类中的sell方法又调用InvocationHandle接口的的子实现类对象的invoke方法
invoke方法通过反射执行了真实对象所属的类(TrainStation)的sell方法
还是上面的案例,如果没有定义SellTicket接口,只定义了TrainStation。很显然JDK代理是不行了
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。
需要引入坐标
- <dependency>
- <groupId>cglib</groupId>
- <artifactId>cglib</artifactId>
- <version>2.2.2</version>
- </dependency>
代码:
- public class ProxyFactory implements MethodInterceptor {
- //声明火车站对象
- private TrainStation trainStation = new TrainStation();
-
- public TrainStation getProxyObject() {
- //创建Enhancer对象,类似JDK代理中Proxy类
- Enhancer enhancer = new Enhancer();
- //设置父类的字节码对象
- enhancer.setSuperclass(TrainStation.class);
- //设置回调函数
- enhancer.setCallback(this);
- //创建代理对象
- TrainStation proxyObject = (TrainStation) enhancer.create();
- return proxyObject;
- }
-
- @Override
- public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
- //对目标对象方法的增强
- System.out.println("增强!");
- method.invoke(trainStation, objects);
- return null;
- }
- }
JDK代理 VS CGLIB代理
使用CGLIB实现动态代理,CGLIB底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是:CGLIB不能对声明为final的类或者方法进行代理,因为CGLIB原理是动态生成被代理类的子类。
在JDK1.6,1.7,1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候1.6和1.7比CGLIB代理效率低一点,但是到了JDK1.8,JDK代理效率高于CGLIB代理,所以如果有接口使用JDK代理,否则使用CGLIB代理
动态代理 VS 静态代理
动态代理和静态代理相比较,最大的好处是接口中声明的所有方法都转移到调用处理器一个集中的方法中处理,这样在接口方法数量比较多的时候,我们就可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题。
优点:
代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
代理对象可以扩展目标对象的功能;
代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点:
增加了系统的复杂度;
使用场景
远程代理:本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常,为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节
防火墙代理
当你将浏览器配置成代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
保护代理
控制对一个对象的访问,如果需要,可以给不同的用户提供不同的权限级别。
如果去欧洲国家旅游的话,他们的插座如下图左边所示,是欧洲标准。而我们使用的插头如下图左边。因此我们是不能直接充电的,所以就需要一个插座转化器,生活中这样的例子很多,手机充电器(将220v转换为5v的电压),读卡器等其实就使用到了适配器模式。
定义:
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少。
适配器模式包含的角色
目标接口:当前系统业务所期待的接口,它可以是抽象类或接口
适配者类:它是被访问和适配的现存组件库中的组件接口
适配器类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
例子:读卡器
目标接口
- public interface SDCard {
- String readSD();
-
- void writeSD(String msg);
- }
适配者类
- public class SDCardImpl implements SDCard {
- String content = "";
-
- @Override
- public String readSD() {
- return content;
- }
-
- @Override
- public void writeSD(String msg) {
- content += msg;
- }
- }
适配器类
- public class SDAdapterTF extends TFCardImpl implements SDCard {
- @Override
- public String readSD() {
- System.out.println("adapter read tfCard");
- return readTF();
- }
-
- @Override
- public void writeSD(String msg) {
- System.out.println("adapter write tfCard");
- writeTF(msg);
- }
- }
Computer
- public class Computer {
- public String readSDCard(SDCard sdCard) {
- return sdCard.readSD();
- }
- }
测试:
就实现了Computer读写TF卡的适配
类适配器模式违反了合成复用原则,类适配器是客户类有一个接口规范的情况下可用,反之不可用
实现方式:对象适配器模式可以采用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口,如果客户类没有接口规范则直接继承目标类
修改适配器类
- //也可以直接实现业务接口SDCard
- public class SDAdapterTF extends SDCardImpl {
- //声明适配者类
- private TFCard tfCard;
-
- public SDAdapterTF(TFCard tfCard) {
- this.tfCard = tfCard;
- }
-
- @Override
- public String readSD() {
- System.out.println("adapter read tfCard");
- return tfCard.readTF();
- }
-
- @Override
- public void writeSD(String msg) {
- System.out.println("adapter write tfCard");
- tfCard.writeTF(msg);
- }
- }
测试:
还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter,实现所有方法,而此时我们只需要继承该抽象类即可。
JDK8后接口可以定义默认方法,也就没必要使用接口适配器模式了
应用场景
以前开发的系统存在满足新系统功能需求的类,但其接口和新系统的接口不一致
使用第三方提供的组件,但组件接口定义的和自己要求的接口定义不同
Reader(字符流),InputStream(字节流)的适配使用的时InputStreamReader
InputStreamReader继承自java.io包中的Reader,对它的抽象或为实现的方法给出实现如:
如上代码中的sd(StreamDecoder类对象),在sun的JDK实现中,实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装,类结构图如下
从上图可以看出,InputStreamReader是对同样实现Reader的StreamDecoder的封装。
StreamDecoder是Sun JDK给出的自身实现,它是适配器类,其中聚合了适配者类InputStream,而Reader则是目标接口的实现类。这个类实现了字节流和字符流之间的转换,采用了对象适配器模式。
例子:快餐店
快餐店由很多快餐。可以加各种配菜,这就使得计算价格很麻烦,传统方式的实现:
使用继承的方式存在的问题:
扩展性不好,如果需要在加一种配料,需要给FiredRice和FiredNoodles分别定义一个子类,如果要新增一个主食,就要定义更多子类,产生过多的子类。
装饰者模式定义:指在不改变现有对象结构的情况下,动态的给该对象增加一些职责的模式
装饰者模式包含的角色:
抽象构件:定义一个抽象接口以规范准备附加责任的对象
具体构件:实现抽象构件,通过装饰角色为其添加一些职责
抽象装饰:继承或实现抽象构建,并包含具体构件的实例,可以通过其子类扩展具体构建
具体装饰:实现抽象装饰的相关方法,并给具体构件对象添加附加责任
使用装饰者模式对快餐店案例进行改进
抽象构件
- public abstract class FastFood {
- private float price;
- private String desc;
-
- public FastFood() {
- }
-
- public FastFood(float price, String desc) {
- this.price = price;
- this.desc = desc;
- }
-
- public float getPrice() {
- return price;
- }
-
- public void setPrice(float price) {
- this.price = price;
- }
-
- public String getDesc() {
- return desc;
- }
-
- public void setDesc(String desc) {
- this.desc = desc;
- }
-
- public abstract float cost();
- }
具体构件
- public class FiredRice extends FastFood {
- public FiredRice() {
- super(10, "炒饭");
- }
-
- @Override
- public float cost() {
- return getPrice();
- }
- }
抽象装饰 Garnish:装饰
- public abstract class Garnish extends FastFood {
- //声明快餐类的变量
- private FastFood fastFood;
-
- public Garnish(float price, String desc, FastFood fastFood) {
- super(price, desc);
- this.fastFood = fastFood;
- }
-
- public FastFood getFastFood() {
- return fastFood;
- }
-
- public void setFastFood(FastFood fastFood) {
- this.fastFood = fastFood;
- }
- }
具体装饰
- public class Egg extends Garnish {
-
- public Egg(FastFood fastFood) {
- super(1, "鸡蛋", fastFood);
- }
-
- @Override
- public float cost() {
- return getPrice() + getFastFood().cost();
- }
-
- @Override
- public String getDesc() {
- return super.getDesc() + getFastFood().getDesc();
- }
- }
测试:
好处:装饰者模式可以带来比继承更加灵活的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。
装饰类和被装饰类额可以独立发展,不会相互耦合,装饰者模式是继承的一个代替模式,装饰者可以动态扩展一个实现类的功能
当不能使用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
不能采用继承的情况注意有两类:
系统中存在大量独立的扩展,为支持每一种组合将产生大量子类,使其爆炸性增长
因为类定义不能继承(如final类)
在不影响其他的对象的情况下,以动态,透明的方式给单个对象添加职责。
在对象的功能要求可以动态的添加,也可以动态的撤销时。
IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter
查看它们的结构
BufferedWriter(具体装饰)使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提供了写数据的效率。
相同点:
都要实现与目标类相同的业务接口
在代理类和具体装饰类中都要声明目标对象
都可以在不修改目标类的前提下增强目标的方法
不同点:
目的不同
装饰者是为了增强目标对象
静态代理是为了保护和隐藏目标对象
获取目标对象构建的地方不同
装饰者是由外界传递来的,可以通过构造方法传递
静态代理是在代理类内部创建,以此来隐藏目标对象
现在有一个需求,需要创建不同的图形,并且每个图形可能会有不同的颜色。我们可以利用继承的方式来设计类的关系如下
我们发现有很多类,假如我们在增加一个形状或颜色,就需要创建更多的类。
试想,在一个有多种可能会变化的维度的系统中,用继承的方式会造成类爆炸,扩展起来不灵活,为了灵活的设计系统,我们此时可以考虑使用桥接模式。
定义:将抽象与实现分离,是它们可以独立变化。它使用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
桥接模式包含的角色:
抽象化角色:定义抽象类,并包含一个对实现化对象的引用
扩展抽象化角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法
实现化角色:定义实现化角色的接口,是扩展抽象化角色调用
具体实现化角色:给出实现化角色接口的具体实现
例子:视频播放器
需要开发一个跨平台视频播放器,可以在不同操作系统平台上播放多种格式的视频文件,常见的视频格式包括RMVB,AVI,WMV等,该播放器包含了两个维度,适合使用桥接模式
代码
实现化角色
- public interface VideoFile {
- void decode(String fileName);
- }
具体实现化角色
- public class AVIFile implements VideoFile {
- @Override
- public void decode(String fileName) {
- System.out.println("AVI视频文件:" + fileName);
- }
- }
抽象化对象
- public abstract class OperatingSystem {
- //声明VideoFile变量
- protected VideoFile videoFile;
-
- public OperatingSystem(VideoFile videoFile) {
- this.videoFile = videoFile;
- }
-
- public abstract void play(String fileName);
- }
扩展抽象化角色
- public class Windows extends OperatingSystem {
- public Windows(VideoFile videoFile) {
- super(videoFile);
- }
-
- @Override
- public void play(String fileName) {
- videoFile.decode(fileName);
- }
- }
测试
好处:
桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要改变原有系统
实现细节对客户透明化
使用场景:
当一个类存在两个独立变化的维度,且两个维度都需要进行扩展
当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时
当一个系统需要在构件的抽象化角色和具体角色之间增加更多的灵活性时,避免两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一层联系
很多人炒股,但大部分人不懂,就会亏。此时基金就是个好帮手,他将投资分散的资金集中起来,交由专业的经理人进行管理。
定义:
又名门面模式,它是通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一的接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
外观模式是”迪米特法则“的经典运用
外观模式包含的角色
外观角色:为多个子系统对外提供一个共同的接口
子系统角色:实现系统的部分功能,客户可以通过外观角色访问它
例子:智能家电控制
使用只能音响控制智能家电的开关
代码
外观角色
- public class SmartAppliancesFacade {
- //聚合子系统对象
- private Light light;
- private TV tv;
-
- public SmartAppliancesFacade() {
- this.light = new Light();
- this.tv = new TV();
- }
-
- public void say(String message) {
- if (message.contains("打开")) {
- on();
- } else if (message.contains("关闭")) {
- off();
- } else {
- System.out.println("说的什么鸟语,老子听不懂");
- }
- }
-
- private void on() {
- light.on();
- tv.on();
- }
-
- private void off() {
- light.off();
- tv.off();
- }
- }
子系统角色
- public class Light {
- public void on() {
- System.out.println("打开电灯");
- }
-
- public void off() {
- System.out.println("关闭电灯");
- }
- }
测试
优点:
降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更容易
缺点:
不符合开闭原则,修改很麻烦
使用场景:
对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系
当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问
当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性
使用tomcat作为web容器时,接收浏览器发送过来的请求封装成一个request,这个request是ServletRequest的子接口HttpServletRequest接口的子实现类,它是RequestFacade类的对象
为什么在此处使用外观模式呢?
定义RequestFacade类,分别实现ServletRequest,同时定义私有成员变量Request,并且方法的实现调用Request的实现,然后将RequestFacade上转为ServletRequest传给Servlet的service方法,这样即使在servlet中被下转为RequestFacade也不能访问私有成员变量对象中的方法。既用了Request,又能防止其中方法被不合理的访问
对于上图,我们可以看作一个文件系统,这样的结构称为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到了某个叶子节点后,就可以对叶子节点进行相关操作。可以将这棵树理解成一个大的容器,容器里面包含很多成员对象,这些成员对象既可以是容器对象也可以是叶子对象,但是由于容器对象和叶子对象功能上的差别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户他希望能够一致的对待容器对象和叶子对象。
定义:又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用于表示部分以及整体层次,这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
组合模式包含的角色
抽象根节点:定义系统各层次对象的公有方法和属性,可以预先定义一些默认行为和属性
树枝节点:定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个属性结构
叶子节点:叶子节点对象,其下再无分支,是系统层次遍历的最小单位
例子:软件菜单
如下图,我们在访问一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(指不在包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单和菜单项
实现
代码
不管是菜单还是菜单项,都应该继承自统一的接口,这里姑且将这个统一的接口称为菜单组件
抽象根节点
- public abstract class MenuComponent {
- //菜单组件的名称
- protected String name;
- //菜单组件的层级
- protected int level;
-
- //添加子菜单
- public void add(MenuComponent menuComponent) {
- throw new UnsupportedOperationException();
- }
-
- //移除子菜单
- public void remove(MenuComponent menuComponent) {
- throw new UnsupportedOperationException();
- }
-
- //获取指定子菜单
- public MenuComponent getChild(int index) {
- throw new UnsupportedOperationException();
- }
-
- //获取菜单或者菜单项的名称
- public String getName() {
- return name;
- }
-
- //打印菜单组件名称的方法
- public abstract void print();
- }
树枝节点
- public class Menu extends MenuComponent {
- private List<MenuComponent> components = new ArrayList<>();
-
- public Menu(String name, int level) {
- this.name = name;
- this.level = level;
- }
-
- @Override
- public void add(MenuComponent menuComponent) {
- components.add(menuComponent);
- }
-
- @Override
- public void remove(MenuComponent menuComponent) {
- components.remove(menuComponent);
- }
-
- @Override
- public MenuComponent getChild(int index) {
- return components.get(index);
- }
-
- @Override
- public void print() {
- //打印菜单名称
- for (int i = 1; i < level; i++) {
- System.out.print("--");
- }
- System.out.println(name);
- //打印菜单名称或子菜单项
- for (MenuComponent component : components) {
- component.print();
- }
- }
- }
叶子节点
- public class MenuItem extends MenuComponent {
-
- public MenuItem(String name, int level) {
- this.name = name;
- this.level = level;
- }
-
- @Override
- public void print() {
- //输出菜单项名
- for (int i = 1; i < level; i++) {
- System.out.print("--");
- }
- System.out.println(name);
- }
- }
测试
在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式
组合模式可以清楚的定义分层次的复杂对象,表示对象的全部或部分层次,他让客户端忽略了层次的差异,方便对整个层次结构进行控制。
客户端可以一致的使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
在组合模式中新增加树枝节点和叶子节点都很方便,无需对现有类库进行任何修改,符合开闭原则
组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单
使用场景:
组合模式正是应树形结构而生,所有组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率。
享元模式存在两种状态
内部状态,即不会随着环境的改变而改变的可共享部分
外部状态,指随环境改变而改变的不可以共享的部分,享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
享元模式的主要有以下角色:
抽象享元角色:通常是一个接口或抽象类,在抽象享元类中声明了具体的享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)
具体享元角色:它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
非享元角色:并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可以设计为非共享具体享元类;当需要一个非共享具体享元类的对象时,可以直接通过实例化创建
享元工厂角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的化则创建一个新的享元对象
例子:俄罗斯方块
如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式来进行实现。
代码
将不同的形状向上抽取出AbstractBox,用来定义共性的属性和方法
抽象享元角色
- public abstract class AbstractBox {
- //获取图形的方法
- public abstract String getShape();
-
- //显示图形和颜色
- public void display(String color) {
- System.out.println("方块形状:" + getShape() + ",颜色:" + color);
- }
- }
具体享元角色
- public class IBox extends AbstractBox {
- @Override
- public String getShape() {
- return "I";
- }
- }
享元工厂角色
- public class BoxFactory {
- private HashMap<String, AbstractBox> map;
-
- //在构造方法中进行初始化
- private BoxFactory() {
- map = new HashMap<>();
- map.put("I", new IBox());
- map.put("L", new LBox());
- map.put("O", new OBox());
- }
-
- private static BoxFactory boxFactory = new BoxFactory();
-
- //获取工厂类对象
- public static BoxFactory getInstance() {
- return boxFactory;
- }
-
- //根据名称获取图形对象
- public AbstractBox getShape(String name) {
- return map.get(name);
- }
- }
测试
优点:
极大减少内存中相似或相同对象的数量,节约系统资源,提高系统性能
享元模式中的外部状态相对独立,且不影响内部状态
缺点:
为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和内部状态,使程序逻辑复杂
使用场景:
一个系统有大量相同或相似的对象,造成内存大量耗费
对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式
第一个输出语句输出的是true,第二个输出语句输出的是false
使用反编译工具查看代码如下
发现给Integer类型的变量赋值的操作底层使用的是valueof()方法,找到该方法
看到它在静态代码块中创建了-128-127之间数的Integer对象,当调用valueOf时,如果参数在-128-127之间则计算下标并从内存中返回,否则创建一个新的Integer对象。