设计模式 12 · 享元模式
结构型模式的最后一站,是享元模式(Flyweight)。它和前面几个模式关注点不太一样——前面的代理、装饰、适配、组合、外观,主要解决的是结构、耦合、灵活性问题;而享元,解决的是一个非常具体、非常物理的问题:内存。当系统里存在海量的、重复的相似对象,把内存吃紧了,享元教你怎么用少量对象扛住大量场景。“Flyweight这个词,原意是拳击里的最轻量级”。这个命名很传神——享元模式的核心,就是想尽办法让对象变得尽可能轻,轻到可以被大量共享。它的思路一句话:很多对象看起来各不相同,但它们的大部分数据其实是重复的、可以共享的;把这些共享的部分提取出来,做成一个共享对象,让所有需要它的地方都指向同一个实例,而不是各自复制一份。举个最能体现威力的例子:电商大促,系统里瞬间涌入一百万个订单,每个订单都关联着商品信息(SKU 名称、图片地址、品牌、规格描述……)。如果每个订单都完整地存一份 SKU 数据,而这一百万个订单其实来来回回就买那几千种热门商品——那内存里就躺着大量内容完全一样的 SKU 副本,纯属浪费。享元模式让这几千种 SKU 数据只在内存里存一份,一百万个订单共享它们。这就是享元要干的事。这篇文章按这条线索展开:先看海量重复对象如何撑爆内存;再引出享元的核心思想——区分内部状态和外部状态;然后给出实现,并讲清那个关键的享元工厂 缓存池;接着揭示一个你天天在用的享元——Integer缓存和String常量池;最后给出适用边界,并为整个结构型模式做收官。贯穿例是订单里的 SKU 共享。目录海量重复对象:内存的隐形杀手享元的核心:内部状态 vs 外部状态实现:享元工厂与共享池你天天在用的享元:Integer 缓存与字符串常量池什么时候用享元结构型模式总收官一、海量重复对象:内存的隐形杀手先看问题。订单关联商品,一个直觉的写法是每个订单项都持有完整的 SKU 信息:publicclassOrderItem{privatelongorderId;privateintcount;// 买了几件(每个订单项不同)// 下面这些是 SKU 信息,每个 OrderItem 都存了一整份privateStringskuName;// iPhone 15 Pro 256G 黑色privateStringskuImage;// 图片地址(一长串 URL)privateStringbrand;// AppleprivateStringspec;// 规格描述(可能很长)// ...}问题出在哪?大促时一百万个OrderItem,但热门商品其实就那几千种。这意味着——同一个 SKU 的名称、图片、品牌、规格,在内存里被重复存储了成千上万次。iPhone 15 Pro 256G 黑色这个字符串、那一长串图片 URL,在一百万个订单里可能重复了几十万遍。这些重复副本白白占着内存,是压垮堆内存的隐形杀手。关键的观察是:这些 SKU 数据,对于同一种商品来说是完全相同、且不会变的。一百万个订单项里,买 iPhone 的那几十万个,它们的skuName、skuImage、brand、spec一模一样。既然一样,为什么要存几十万份?存一份,让它们都指向这一份,不就行了?这就是享元的出发点。但要做到共享,我们得先把OrderItem里的数据分成两类:哪些是每个订单项都相同、可以共享的,哪些是每个订单项各不相同、必须独立的。这个区分,就是享元模式的灵魂。二、享元的核心:内部状态 vs 外部状态享元模式把一个对象的状态,划分成两部分:内部状态(Intrinsic State):不随环境变化、可以共享的部分。它是对象的固有属性,和使用它的具体场景无关。在我们的例子里,SKU 的名称、图片、品牌、规格,就是内部状态——不管哪个订单用它,这些都一样。外部状态(Extrinsic State):随环境变化、不能共享的部分。它依赖于具体的使用场景,每次使用都可能不同。在例子里,买了几件count、属于哪个订单orderId,就是外部状态——每个订单项都不一样。享元模式的做法就是:把内部状态提取出来,做成一个可共享的享元对象,在内存里只存一份;而外部状态则不放进享元里,由调用方在使用时从外部传入。用一张图看这个拆分 共享的核心思想最清楚:图里的关键就是那次拆分:内部状态(SKU 数据)→ 提取成共享的SkuFlyweight,几千种商品只存几千个对象;外部状态(数量、订单号)→ 留在OrderItem里,一百万个订单项各存各的,但它们的 SKU 字段只是一个指向共享享元的引用。这样一来,内存里 SKU 数据从一百万份骤降到几千份,而每个订单项只多存一个引用(几个字节)。用极小的引用开销,换掉了海量的重复数据——这就是享元省内存的原理。三、实现:享元工厂与共享池理解了内外状态的划分,实现就水到渠成了。分三步。第一步,定义享元类(只包含可共享的内部状态):// 享元:只存不变的、可共享的 SKU 内部状态publicclassSkuFlyweight{privatefinallongskuId;privatefinalStringskuName;privatefinalStringskuImage;privatefinalStringbrand;publicSkuFlyweight(longskuId,StringskuName,StringskuImage,Stringbrand){this.skuIdskuId;this.skuNameskuName;this.skuImageskuImage;this.brandbrand;}// 只有 getter,没有 setter —— 享元必须是不可变的(见下方提醒)}第二步,享元工厂(维护一个缓存池,保证同一个 SKU 只创建一次):publicclassSkuFactory{// 缓存池:key 是 skuId,value 是共享的享元对象privatestaticfinalMapLong,SkuFlyweightpoolnewConcurrentHashMap();// 获取享元:池里有就直接返回,没有才创建并放入池publicstaticSkuFlyweightgetSku(longskuId,Stringname,Stringimage,Stringbrand){returnpool.computeIfAbsent(skuId,id-newSkuFlyweight(id,name,image,brand));}}这个工厂是享元模式的关键——它保证了同一个 SKU 全局只有一个实例。第一次请求某个 SKU 时创建并缓存,以后所有请求都返回同一个对象。你会发现,这其实和第 6 篇原型模式的原型注册表、乃至广义的缓存思想是相通的。第三步,订单项持有享元引用 自己的外部状态:publicclassOrderItem{privatelongorderId;// 外部状态privateintcount;// 外部状态privatefinalSkuFlyweightsku;// 指向共享享元(不是复制!)publicOrderItem(longorderId,intcount,longskuId,...){this.orderIdorderId;this.countcount;this.skuSkuFactory.getSku(skuId,...);// 从工厂拿共享的那一份}}现在,一百万个买 iPhone 的订单项,它们的sku字段全都指向内存里同一个SkuFlyweight对象。SKU 数据只存了一份。一个必须遵守的铁律:享元对象(内部状态)必须是不可变的。想想看,既然一个SkuFlyweight被一百万个订单共享,如果它是可变的、某个订单不小心改了它的skuName,那所有共享它的订单的商品名就全被改了——这是灾难性的串改(和第 6 篇浅拷贝串味是同一类问题)。所以享元类必须字段全final、无 setter、做成不可变对象。这里,第 1 篇讲的不可变对象天生线程安全、可放心共享再一次成了享元能成立的基石。四、你天天在用的享元:Integer 缓存与字符串常量池享元听起来像个高级优化,但其实你每天都在用 JDK 内置的享元,只是没意识到。这里有两个最经典的:其一,Integer的缓存(IntegerCache)。看这段经常让人困惑的代码:Integera127,b127;System.out.println(ab);// trueIntegerc128,d128;System.out.println(cd);// false !为什么 127 相等、128 就不等了?因为Integer用了享元模式:Integer.valueOf()内部维护了一个缓存池,默认缓存了−128 到 127之间的所有Integer对象。当你用一个在这个范围内的值时(自动装箱会调valueOf),拿到的是缓存里同一个共享对象,所以a b为true;而 128 超出了缓存范围,每次都new一个新对象,所以c d为false。这里 −128~127 这个区间的整数值就是内部状态,被提前创建好、共享复用——这是享元模式在 JDK 里最著名的应用,也是为什么比较 Integer 要用equals而不是这个经典坑的根源。其二,字符串常量池(String Pool)。你写的所有字符串字面量,都被享元化了:Strings1hello;Strings2hello;System.out.println(s1s2);// true,指向常量池里同一个 helloJVM 维护了一个字符串常量池,相同内容的字符串字面量在池里只存一份,所有引用都指向它。这就是为什么两个hello字面量为true——它们共享了常量池里的同一个String对象。字符串内容(不可变!)就是内部状态。String 的不可变性,正是它能被安全地放进常量池共享的前提,又一次印证了享元必须不可变这条铁律。其他还有Boolean.valueOf()、Long/Short/Byte/Character的缓存等,思路都一样。认出这些,你就明白享元不是什么屠龙之技,而是就藏在你天天写的代码底下。五、什么时候用享元老规矩,泼冷水。享元是所有模式里最该谨慎使用的一个,因为它是一种用复杂度换内存的优化,而优化必须有的放矢。适合用的信号(几个条件最好同时满足):系统里存在海量的对象(成千上万、乃至百万级),多到内存吃紧;这些对象里,大部分状态是重复的、可共享的(内部状态占比高);对象的状态能被清晰地拆分成内部状态和外部状态;剥离外部状态后,共享对象的种类数远小于对象总数(几千种 SKU 撑起一百万订单)。不该用的信号:对象数量不大——那享元带来的内存节省微乎其微,却凭空增加了工厂、内外状态拆分的复杂度,得不偿失;对象的状态大部分是外部的、可共享的部分很少——那没什么可共享的,享元无从发挥;状态无法清晰拆分——强行拆会让代码难以理解。这里要特别强调,享元是全系列里最典型的不要过早优化的例子。它不是用来提升代码优雅度的模式,而是用来解决实实在在的内存压力的。先用性能分析工具确认内存确实被海量重复对象吃掉了这个事实,再上享元。在没有真实内存瓶颈时就套享元,是拿代码复杂度去换一个不存在的收益——比过度设计还冤,因为你连以防万一的借口都没有。记住:享元是优化手段,优化的前提是先有可度量的问题。六、结构型模式总收官第 12 篇结束,七个结构型模式全部讲完,做个总收官。结构型模式关心的都是类和对象怎么组合、连接,但各自解决的问题不同,可以分成两组来记:第一组:“包装一个对象”(通过包装改变对象的表现)模式一句话核心意图代理 Proxy给对象一个替身控制访问(日志/权限/事务)装饰器 Decorator像洋葱层层包裹动态增强功能适配器 Adapter接口转接头转换接口,让不兼容的能协作第二组:“组织一群对象”(处理多个对象/子系统的关系)模式一句话核心意图组合 Composite单个和一组长得一样统一处理树形结构外观 Facade给子系统盖门面简化复杂子系统的使用享元 Flyweight提取共享,省内存用少量对象扛住海量场景桥接 Bridge抽象与实现分离让两个维度独立变化(下一篇讲)注:桥接模式是结构型第七个,我们放在下一篇讲它解决的是多维度变化的问题。)回望这七个模式,你会发现它们共享着几条贯穿全系列的主线:组合优于继承在代理、装饰、适配、桥接里反复做选择;迪米特法则是外观的灵魂;不可变对象是享元的基石;而面向接口 隔离变化,则是它们共同的底色。这再次说明:模式不是二十三个孤立的技巧,而是那几条设计原则在不同场景下结出的果实。小结。享元模式专治海量重复对象吃内存的问题:把对象状态拆成可共享的内部状态和因场景而异的外部状态,把内部状态提取成一个不可变的共享享元、由工厂缓存池保证全局唯一,让海量对象共享少数享元,用极小的引用开销换掉海量的重复数据。Integer的 −128~127 缓存、字符串常量池,就是你天天在用的享元。它是一种用复杂度换内存的优化,必须在确认真实内存瓶颈后才使用,是不要过早优化最好的注脚。至此,结构型七模式(含下一篇的桥接)成型。下一篇我们讲桥接模式——当一个东西同时在两个维度上变化时(比如订单类型和通知渠道),继承会导致维度相乘的类爆炸,桥接教你把两个维度拆开、让它们各自独立变化。

相关新闻

Spring Boot实战:电商盲盒系统概率算法与事务处理完整实现

Spring Boot实战:电商盲盒系统概率算法与事务处理完整实现

最近在开发一个盲盒类电商项目时,遇到了一个有趣的挑战:如何高效、优雅地实现“拆盲盒”这个核心交互,特别是当盲盒内物品是“刀”和“盾”这类组合道具时,背后的概率计算、库存扣减、订单生成与道具发放逻辑环环相扣,…

2026/8/7 3:48:11 阅读更多 →
3步解锁你的网易云音乐:NCM格式解密转换终极指南

3步解锁你的网易云音乐:NCM格式解密转换终极指南

3步解锁你的网易云音乐:NCM格式解密转换终极指南 【免费下载链接】ncmdumpGUI C#版本网易云音乐ncm文件格式转换,Windows图形界面版本 项目地址: https://gitcode.com/gh_mirrors/nc/ncmdumpGUI 还在为网易云音乐下载的歌曲只能在特定客户端播放…

2026/8/7 3:47:11 阅读更多 →
EC200N-CN Cat.1模组从零上手:硬件连接、AT命令调试与网络通信实战

EC200N-CN Cat.1模组从零上手:硬件连接、AT命令调试与网络通信实战

1. 项目概述:从零上手EC200N-CN模组 最近在做一个物联网小项目,需要用到4G Cat.1通信模组,选来选去,最终敲定了移远通信的EC200N-CN。这玩意儿在物联网圈子里算是“国民级”的模组了,性价比高,资料也相对齐…

2026/8/7 3:47:11 阅读更多 →

最新新闻

WPF桌面应用开发:IconFont图标库集成与实战指南

WPF桌面应用开发:IconFont图标库集成与实战指南

1. 项目概述:为什么WPF项目需要IconFont图标库?做WPF桌面应用开发,界面美化是个绕不开的坎。早年我们可能到处找PNG图标,不同尺寸要准备多套,颜色想换个主题就得重做一套图,不仅资源文件臃肿,维…

2026/8/7 4:38:38 阅读更多 →
哈希表线性探测法:从原理到工程实践的性能调优指南

哈希表线性探测法:从原理到工程实践的性能调优指南

1. 从“撞车”到“找车位”:开放地址法的现实隐喻在停车场找车位,大概是每个司机都经历过的日常。你开着车,按照既定的车位编号(比如A-01)开过去,发现那里已经停了一辆车。这时候,你不会掉头就走…

2026/8/7 4:38:38 阅读更多 →
技术团队如何实践“丧式加油”:从情绪管理到工程落地的韧性协作框架

技术团队如何实践“丧式加油”:从情绪管理到工程落地的韧性协作框架

在实际的技术开发与工程实践中,我们常常会遇到一些看似非技术性的概念,却能深刻影响团队协作、项目氛围乃至最终产品的质量。“丧式加油”这个源自流行文化的词汇,虽然描述的是一个偶像团体的标志性动作,但其内核——一种在压力下…

2026/8/7 4:38:38 阅读更多 →
Linux tee命令与管道实战:高效管理命令行输出与Python pip安装

Linux tee命令与管道实战:高效管理命令行输出与Python pip安装

在Linux命令行或Windows PowerShell中,你是否曾遇到过这样的场景:一个命令的输出,你既想实时看到它,又想把它保存到文件里?或者,你想把一个长命令的输出作为另一个命令的输入,但又不想丢失中间结…

2026/8/7 4:38:38 阅读更多 →
CCS工程自动生成BIN文件:TMS320F28335项目构建后步骤配置详解

CCS工程自动生成BIN文件:TMS320F28335项目构建后步骤配置详解

1. 从HEX到BIN:为什么28335项目需要这个转换?如果你是从CCS3.3或者更老的版本迁移到CCS5、CCS6甚至CCS12的工程师,大概率会遇到一个“小”问题:以前工程编译后,在Debug或Release文件夹里,那个可以直接用于串…

2026/8/7 4:38:38 阅读更多 →
企业级本地AI部署实践:基于RAG与飞书集成的智能协作助手构建

企业级本地AI部署实践:基于RAG与飞书集成的智能协作助手构建

1. 项目缘起:从“云端焦虑”到“本地觉醒”最近和几个不同行业的技术负责人聊天,发现一个挺有意思的共同点:大家聊起AI,兴奋劲儿过去之后,眉头都皱起来了。兴奋是因为大模型的能力确实肉眼可见,能写代码、能…

2026/8/7 4:37:37 阅读更多 →

日新闻

为什么scrcpy成为Android投屏的终极解决方案:完整实战指南

为什么scrcpy成为Android投屏的终极解决方案:完整实战指南

为什么scrcpy成为Android投屏的终极解决方案:完整实战指南 【免费下载链接】scrcpy Display and control your Android device 项目地址: https://gitcode.com/GitHub_Trending/sc/scrcpy 想要将Android手机屏幕完美投射到电脑上,享受大屏操作的自…

2026/8/7 0:00:19 阅读更多 →
如何在5分钟内掌握Tom Select:打造现代化表单选择器的终极指南

如何在5分钟内掌握Tom Select:打造现代化表单选择器的终极指南

如何在5分钟内掌握Tom Select:打造现代化表单选择器的终极指南 【免费下载链接】tom-select Tom Select is a lightweight (~16kb gzipped) hybrid of a textbox and select box. Forked from selectize.js to provide a framework agnostic autocomplete widget wi…

2026/8/7 0:00:19 阅读更多 →
5分钟快速上手:NSZ压缩工具终极指南,轻松管理Switch游戏文件

5分钟快速上手:NSZ压缩工具终极指南,轻松管理Switch游戏文件

5分钟快速上手:NSZ压缩工具终极指南,轻松管理Switch游戏文件 【免费下载链接】nsz NSZ - Homebrew compatible NSP/XCI compressor/decompressor 项目地址: https://gitcode.com/gh_mirrors/ns/nsz 你是否在为Nintendo Switch游戏文件占用大量存储…

2026/8/7 0:00:19 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/6 22:02:27 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/6 22:02:27 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/6 22:02:27 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/5 23:28:39 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/6 22:02:28 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/5 23:46:51 阅读更多 →