一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网
https://yccoding.com/
享元模式是一种用于性能优化的设计模式,通过共享相同或相似对象来减少内存占用。本文档详细介绍了享元模式的基础概念、实现原理、应用场景及优缺点,并通过具体例子如Integer、String、线程池和Handler等展示了其实际应用。
此外,还探讨了享元模式与其他设计模式的结合使用,以及在休闲棋类和文本编辑器中的应用。适合需要优化系统性能和资源利用率的开发者参考。
面向对象技术可以很好地解决一些灵活性或可扩展性问题,但在很多情况下需要在系统中增加类和对象的个数。
当对象数量太多时,将导致运行代价过高,带来性能下降等问题。举一个简单例子,比如handler发送消息,创建大量的消息message对象,就用到了享元模式。
享元模式由来,主要是解决对象太多创建的问题:更多内容
享元模式(Flyweight Pattern):
运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。
享元模式的核心思想是将具有相同内部状态的对象共享,以减少内存占用。更多内容
在享元模式中,对象分为两种类型:内部状态(Intrinsic State)和外部状态(Extrinsic State)。
内部状态是对象共享的部分,它不会随着外部环境的变化而改变。外部状态是对象特定的部分,它会随着外部环境的变化而变化。
享元模式使用场景
工作中遇到场景
作用
享元模式的一些关键要点:
享元模式的核心思想是将对象的内部状态和外部状态分离。更多内容,内部状态可以被多个对象共享,而外部状态则由客户端传递给享元对象。通过共享内部状态,可以减少对象的数量,从而减少内存占用。
接单做网站:要求做产品展示网站,有的人希望是新闻发布形式的,有的人希望是博客形式的,也有还是原来的产品图片加说明形式的。
因为他们找我们来做的人的需求只是有一些小小的差别。但是不可能有100家企业来找你做网站,你难道去申请100个服务器,用100个数据库,然后用类似的代码复制100遍,去实现吗?
不使用享元模式 - 接单做网站。
public class FlyweightWebSite { public static void main(String[] args) { test1(); } public static void test1() { WebSite fx = new WebSite("产品展示"); fx.use(); WebSite fy = new WebSite("产品展示"); fy.use(); WebSite fz = new WebSite("产品展示"); fz.use(); WebSite fl = new WebSite("博客"); fl.use(); WebSite fm = new WebSite("博客"); fm.use(); WebSite fn = new WebSite("博客"); fn.use(); } //网站 public static class WebSite { private String name = ""; public WebSite(String name) { this.name = name; } public void use() { System.out.println("网站分类:" + name); } } }
如果要做三个产品展示,三个博客的网站,就需要六个网站类的实例,而其实它们本质上都是一样的代码,如果网站增多,实例也就随着增多。更多内容
利用享元模式做成第一版本的网站,代码如下所示:
public static void test2() { System.out.println("使用享元模式做的第一版网站"); WebSiteFactory webSiteFactory = new WebSiteFactory(); AbsWebSite siteCategory = webSiteFactory.getWebSiteCategory("产品展示"); siteCategory.use(); AbsWebSite siteCategory1 = webSiteFactory.getWebSiteCategory("产品展示"); siteCategory1.use(); AbsWebSite siteCategory2 = webSiteFactory.getWebSiteCategory("产品展示"); siteCategory2.use(); AbsWebSite siteCategory3 = webSiteFactory.getWebSiteCategory("博客"); siteCategory3.use(); AbsWebSite siteCategory4 = webSiteFactory.getWebSiteCategory("博客"); siteCategory4.use(); AbsWebSite siteCategory5 = webSiteFactory.getWebSiteCategory("博客"); siteCategory5.use(); System.out.println("总共创建了 " + webSiteFactory.getWebSiteCount() + " 个实例"); } /** * 网站抽象类 */ public abstract static class AbsWebSite { public abstract void use(); } /** * 具体网站类 */ public static class ConcreteWebSite extends AbsWebSite { private String name = ""; public ConcreteWebSite(String name) { this.name = name; } @Override public void use() { System.out.println("网站分类:" + name); } } /** * 网站工厂类 */ public static class WebSiteFactory { private HashMap<String, AbsWebSite> flyweights = new HashMap<>(); /** * 获得网站分类 * * @param key key * @return AbsWebSite */ public AbsWebSite getWebSiteCategory(String key) { if (!flyweights.contains(key)) { flyweights.put(key, new ConcreteWebSite(key)); } return flyweights.get(key); } /** * 获得网站实例个数 * * @return 数量 */ public int getWebSiteCount() { return flyweights.size(); } }
上面案例遇到了一些问题
基本实现了享元模式的共享对象的目的,也就是说,不管建个网站,只要是 ‘产品展示’ ,都是一样的,只要是 ‘博客’ ,也是完全相同的。
但这样是有问题的,以上这样建的网站不是一家客户的,它们的数据不会相同,所以至少它们都应该有不同的账号。这样写没有体现对象间的不同,只体现了它们共享的部分。
内部状态 - 外部状态 - 享元模式 - 接单做网站
在享元对象内部并且不会随环境改变而改变的共享部分,可以称为享元对象的内部状态,随环境改变而改变的、不可以共享的状态就是外部状态。
事实上,享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个 参数外基本上都是相同的,有时就能够大幅度地减少需要实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递进来, 就可以通过共享大幅度地减少单个实例的数目。
也就是说,享元模式 Flyweight 执行时所需的状态有内部的也可能有外部的,内部状态存储于 ConcreteFlyweight 对象之中,而外部对象则应该考虑由客户端对象存储或计 算,当调用Flyweight对象的操作时,将该状态传递给它。
客户的账号就是外部状态,应该由专门的对象来处理。更多内容
/** * 用户类,用于网站的客户账号,是"网站"类的外部状态。 */ public static class User { private String name; public User(String name) { this.name = name; } public String getName() { return name; } } /** * 网站抽象类 */ public abstract static class AbsWebSite2 { public abstract void use(User user); } public static class ConcreteWebSite2 extends AbsWebSite2 { private String name = ""; public ConcreteWebSite2(String name) { this.name = name; } @Override public void use(User user) { System.out.println("网站分类:" + name + " 来自客户:" + user.getName() + "的需求"); } }
打印数据如下所示,这样就可以协调内部与外部状态了。
使用享元模式做的第二版网站,添加外部状态 网站分类:产品展示 来自客户:打工充0的需求 网站分类:产品展示 来自客户:打工充1的需求 网站分类:产品展示 来自客户:打工充2的需求 网站分类:博客 来自客户:打工充3的需求 网站分类:博客 来自客户:打工充4的需求 网站分类:博客 来自客户:打工充5的需求 总共创建了 2 个实例
享元模式实现的角色
享元模式的基本实现代码如下所示
/** * 享元模式基本实现,比较官方的demo案例 */ public class FlyweightDemo { public static void main(String[] args) { int extrinsicState = 30; FlyweightFactory factory = new FlyweightFactory(); Flyweight flyweightA = factory.getFlyweight("A"); flyweightA.operation(--extrinsicState); Flyweight flyweightB = factory.getFlyweight("B"); flyweightB.operation(--extrinsicState); Flyweight flyweightC = factory.getFlyweight("C"); flyweightC.operation(--extrinsicState); // 不要共享的 UnsharedConcreteFlyweight unsharedFly = new UnsharedConcreteFlyweight(); unsharedFly.operation(--extrinsicState); } /** * Flyweight类是所有具体享元类的超类或接口,通过这个接口, Flyweight可以接受并作用于外部状态。 */ public static abstract class Flyweight { public abstract void operation(int extrinsicState); } /** * ConcreteFlyweight是继承Flyweight超类或实现Flyweight接口,并为内部状态增加存储空间。 * 需要共享的具体Flyweight子类 */ public static class ConcreteFlyweight extends Flyweight { @Override public void operation(int extrinsicState) { System.out.println("需要共享的具体Flyweight子类:" + extrinsicState); } } /** * 需要共享的具体Flyweight子类 * UnsharedConcreteFlyweight是指那些不需要共享的Flyweight子类。因为Flyweight接口共享成为可能,但它并不强制共享。 */ public static class UnsharedConcreteFlyweight extends Flyweight { @Override public void operation(int extrinsicState) { System.out.println("不需要共享的具体Flyweight子类:" + extrinsicState); } } /** * 享元工厂 */ public static class FlyweightFactory { private final HashMap<String, Flyweight> flyweights = new HashMap<>(); /** * 初始化工厂三个实例 */ public FlyweightFactory() { flyweights.put("A", new ConcreteFlyweight()); flyweights.put("B", new ConcreteFlyweight()); flyweights.put("C", new ConcreteFlyweight()); } /** * 根据客户端请求,获得已生成的实例 * * @param key key * @return Flyweight */ public Flyweight getFlyweight(String key) { return flyweights.get(key); } } }
在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。
实际上,享元模式有点类似于之前讲到的单例的变体:多例。更多内容
区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。
在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。
这里的“缓存”实际上是“存储”的意思,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。
我们平时所讲的缓存,主要是为了提高访问效率,而非复用。
简单解释一下对象池。
像 C++ 这样的编程语言,内存的管理是由程序员负责的。为了避免频繁地进行对象创建和释放导致内存碎片,我们可以预先申请一片连续的内存空间,也就是这里说的对象池。
每次创建对象时,我们从对象池中直接取出一个空闲对象来使用,对象使用完成之后,再放回到对象池中以供后续复用,而非直接释放掉。
对象池、连接池(比如数据库连接池)、线程池等也是为了复用,那它们跟享元模式有什么区别呢?更多内容
虽然对象池、连接池、线程池、享元模式都是为了复用,但是抠一抠“复用”这个字眼的话,对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。
池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。
享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。
先来看下面这样一段代码。你可以先思考下,这段代码会输出什么样的结果。
private static void test() { Integer i1 = 31; Integer i2 = 31; Integer i3 = 129; Integer i4 = 129; System.out.println(i1 == i2); System.out.println(i3 == i4); //true //false }
你可能会觉得,i1 和 i2 值都是 56,i3 和 i4 值都是 129,i1 跟 i2 值相等,i3 跟 i4 值相等,所以输出结果应该是两个 true。但其实这是错误的。因为Integer用到了缓存池的概念……
需要弄清楚下面两个问题:
所谓的自动装箱,就是自动将基本数据类型转换为包装器类型。所谓的自动拆箱,也就是自动将包装器类型转化为基本数据类型。更多内容
Integer i = 31; //自动装箱 底层执行了:Integer i = Integer.valueOf(31); int j = i; //自动拆箱 底层执行了:int j = i.intValue();
为何 i3==i4 判定语句也会返回 false
这正是因为 Integer 用到了享元模式来复用对象,才导致了这样的运行结果。
当我们通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象的时候,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建。
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
这里的 IntegerCache 相当于生成享元对象的工厂类,只不过名字不叫 xxxFactory 而已。
再来看下,享元模式在 Java String 类中的应用。同样,我们还是先来看一段代码,你觉得这段代码输出的结果是什么呢?
private static void test2() { String s1 = "打工充"; String s2 = "打工充"; String s3 = new String("打工充"); System.out.println(s1 == s2); System.out.println(s1 == s3); //true //false }
跟 Integer 类的设计思路相似,String 类利用享元模式来复用相同的字符串常量(也就是代码中的“打工充”)。
JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。
Handler 消息机制中的 Message 消息池就是使用享元模式复用了 Message 对象。
遇到问题说明:如果使用 new Message() 会构造大量的 Message 对象。通过new创建Message就会创建大量重复的Message对象,导致内存占用率高,频繁GC等问题。
使用享元模式:使用 Message 时一般是用 Message.obtain 来获取消息。通过享元模式创建一个大小为50的消息池,避免了上述问题的产生。
Message享元模式分析:Message相当于承担了享元模式中3个元素的职责,既是Flyweight抽象,又是ConcreteFlyweight角色,同时又承担了FlyweightFactory管理对象池的职责。
注意点:Message的享元模式并不是经典的实现方式,它没有内部,外部状态,集各个职责于一身,甚至更像一个对象池。更多内容
优点
缺点:
享元模式更多的时候是一种底层的设计模式,但现实中也是有应用的:
比如休闲棋类。
如果不用享元模式会怎么样?
像象棋,一盘棋理论上有32棋子,那如果用常规的面向对象方式编程,每盘棋都可能有至少32个棋子对象产生。
一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。
思考一下下面的问题
一台服务器就很难支持更多的玩家玩围棋游戏了,比如1万棋局玩家,那就会有大量的对象,毕竟内存空间还是有限的。
那么我们如何去避免大量细粒度的对象,同时又不影响客户程序,是一个值得去思考的问题。有没有什么办法来节省内存呢?
用了享元模式主要是节省资源和降低损耗
比如说休闲游戏开发中,像象棋,它们都有大量的棋子对象,分析一下,它们的内部状态和外部状态各是什么?更多内容
象棋只有红黑两色、跳棋颜色略多一些,但也是不太变化的,所以颜色应该是棋子的内部状态,而各个棋子之间的差别主要就是位置的不同,所以方位坐标应该是棋子的外部状态。
在使用享元模式之前,记录1万个棋局,我们要创建 32 万(32*1 万)个棋子的对象。利用享元模式,我们只需要创建32个享元对象供所有棋局共享使用即可,大大节省了内存。
使用享元模式的一个常见例子是文本编辑器中的字符对象。
在一个文本文件中,可能有大量的字符对象,它们的外部状态(例如位置、字体、颜色等)可能不同,但内部状态(例如字符代码、字符宽度等)是相同的。
通过共享具有相同字符代码的字符对象,可以大大减少内存使用。
享元模式适用于大量细粒度对象的场景
通过共享对象的内部状态来减少对象数量和内存占用,从而提高系统性能和资源利用率。
它在需要重复创建相似对象的情况下特别有用,并且适用于多线程环境,但需要注意线程安全性。更多内容
在以下情况下可以使用享元模式:
享元模式与其他模式的联用
模块 | 描述 | 备注 |
---|---|---|
GitHub | 多个YC系列开源项目,包含Android组件库,以及多个案例 | GitHub |
博客汇总 | 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 | YCBlogs |
设计模式 | 六大设计原则,23种设计模式,设计模式案例,面向对象思想 | 设计模式 |
Java进阶 | 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM | Java高级 |
网络协议 | 网络实际案例,网络原理和分层,Https,网络请求,故障排查 | 网络协议 |
计算机原理 | 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 | 计算机基础 |
学习C编程 | C语言入门级别系统全面的学习教程,学习三到四个综合案例 | C编程 |
C++编程 | C++语言入门级别系统全面的教学教程,并发编程,核心原理 | C++编程 |
算法实践 | 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 | Leetcode |
Android | 基础入门,开源库解读,性能优化,Framework,方案设计 | Android |
23种设计模式
23种设计模式 & 描述 & 核心作用 | 包括 |
---|---|
创建型模式 提供创建对象用例。能够将软件模块中对象的创建和对象的使用分离 |
工厂模式(Factory Pattern) 抽象工厂模式(Abstract Factory Pattern) 单例模式(Singleton Pattern) 建造者模式(Builder Pattern) 原型模式(Prototype Pattern) |
结构型模式 关注类和对象的组合。描述如何将类或者对象结合在一起形成更大的结构 |
适配器模式(Adapter Pattern) 桥接模式(Bridge Pattern) 过滤器模式(Filter、Criteria Pattern) 组合模式(Composite Pattern) 装饰器模式(Decorator Pattern) 外观模式(Facade Pattern) 享元模式(Flyweight Pattern) 代理模式(Proxy Pattern) |
行为型模式 特别关注对象之间的通信。主要解决的就是“类或对象之间的交互”问题 |
责任链模式(Chain of Responsibility Pattern) 命令模式(Command Pattern) 解释器模式(Interpreter Pattern) 迭代器模式(Iterator Pattern) 中介者模式(Mediator Pattern) 备忘录模式(Memento Pattern) 观察者模式(Observer Pattern) 状态模式(State Pattern) 空对象模式(Null Object Pattern) 策略模式(Strategy Pattern) 模板模式(Template Pattern) 访问者模式(Visitor Pattern) |