在软件开发中,我们经常会遇到需要创建大量相似对象的情况。如果每个对象都独立存储所有数据,将会消耗大量内存资源,导致系统性能下降。享元模式(Flyweight Pattern)正是为解决这一问题而生的经典设计模式。本文将深入探讨享元模式的核心概念、实现原理、应用场景以及实际案例,帮助读者全面理解并掌握这一高效的对象共享技术。

一、享元模式概述

1.1 什么是享元模式

享元模式是一种结构型设计模式,它通过共享技术来有效地支持大量细粒度对象的复用,从而减少内存消耗。该模式的核心思想是将对象的状态分为内部状态(Intrinsic State)和外部状态(Extrinsic State),其中内部状态是可以共享的,而外部状态则由客户端在需要时传递给享元对象。

"Flyweight"一词源于拳击运动中的"轻量级"概念,寓意这种模式创建的对象的"轻量"特性——它们只包含最少量的内部状态,大部分状态由外部提供。

1.2 历史背景

享元模式最早由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides在1994年的经典著作《设计模式:可复用面向对象软件的基础》中提出。这四位作者被称为"四人帮"(Gang of Four,GoF),他们系统化地整理了23种经典设计模式,享元模式是其中之一。

1.3 模式动机

在面向对象编程中,一切皆对象。但当系统中需要创建大量相似对象时,会面临以下问题:

  1. 内存消耗过大:每个对象都占用一定的内存空间,大量对象会迅速耗尽可用内存

  2. 创建开销大:频繁创建和销毁对象会导致性能瓶颈

  3. GC压力大:在垃圾回收环境中,大量对象会增加GC负担

享元模式通过共享技术解决了这些问题,它使得多个对象可以共享相同的状态,而不是每个对象都保存一份副本。

二、享元模式的结构与实现

2.1 模式结构

享元模式包含以下几个关键角色:

  1. Flyweight(抽象享元类):定义对象的接口,声明操作外部状态的方法

  2. ConcreteFlyweight(具体享元类):实现抽象享元接口,存储内部状态

  3. UnsharedConcreteFlyweight(非共享具体享元类):不需要共享的子类

  4. FlyweightFactory(享元工厂类):创建并管理享元对象,确保合理共享

  5. Client(客户端):维护外部状态,并在需要时将外部状态传递给享元对象

2.2 状态划分

享元模式成功的关键在于正确区分内部状态和外部状态:

  • 内部状态(Intrinsic State):存储在享元对象内部且不会随环境改变的状态,可以被共享

  • 外部状态(Extrinsic State):随环境改变而改变的状态,不可共享,由客户端保存

2.3 Java实现示例

// 抽象享元类
interface ChessPiece {void draw(int x, int y); // x,y是外部状态(位置)
}// 具体享元类 - 白棋
class WhiteChessPiece implements ChessPiece {private final String color = "白色"; // 内部状态@Overridepublic void draw(int x, int y) {System.out.println(color + "棋子放置在(" + x + "," + y + ")");}
}// 具体享元类 - 黑棋
class BlackChessPiece implements ChessPiece {private final String color = "黑色"; // 内部状态@Overridepublic void draw(int x, int y) {System.out.println(color + "棋子放置在(" + x + "," + y + ")");}
}// 享元工厂
class ChessPieceFactory {private static final Map<String, ChessPiece> pieces = new HashMap<>();static {pieces.put("白", new WhiteChessPiece());pieces.put("黑", new BlackChessPiece());}public static ChessPiece getChessPiece(String color) {return pieces.get(color);}
}// 客户端
public class ChessGame {public static void main(String[] args) {// 下白棋ChessPiece white1 = ChessPieceFactory.getChessPiece("白");white1.draw(1, 1);ChessPiece white2 = ChessPieceFactory.getChessPiece("白");white2.draw(1, 2);// 下黑棋ChessPiece black1 = ChessPieceFactory.getChessPiece("黑");black1.draw(2, 1);// 再次下白棋ChessPiece white3 = ChessPieceFactory.getChessPiece("白");white3.draw(2, 2);System.out.println("实际创建的棋子对象数量: " + ChessPieceFactory.getPieceCount());}
}

在这个示例中,无论棋盘上有多少白棋或黑棋,系统都只创建了两个棋子对象(一白一黑),所有同颜色的棋子共享同一个对象,只是位置(外部状态)不同。

三、享元模式的深入分析

3.1 内部状态与外部状态的确定

正确区分内部状态和外部状态是应用享元模式的关键。以下是一些判断标准:

  1. 内部状态

    • 对象固有的、不随环境变化的属性

    • 可以被多个对象共享

    • 通常是不变(immutable)的

    • 例如:字符的字形、棋子的颜色、树的种类等

  2. 外部状态

    • 取决于对象所处的上下文环境

    • 每个对象特有的、不可共享的属性

    • 可能会频繁变化

    • 例如:字符的位置、棋子的位置、树的位置等

3.2 线程安全考虑

在多线程环境下使用享元模式时需要注意:

  1. 享元对象通常是不可变的(只有内部状态),因此本质上是线程安全的

  2. 如果享元对象包含可变状态,需要采取同步措施

  3. 享元工厂的创建方法应考虑并发访问问题

3.3 与其他模式的关系

  1. 与单例模式

    • 都可以限制对象的数量

    • 单例模式确保一个类只有一个实例

    • 享元模式可以有多个实例,但相同内部状态的实例被共享

  2. 与组合模式

    • 可以结合使用,共享的享元对象可以作为组合结构的叶子节点

  3. 与状态模式/策略模式

    • 享元对象可以持有对状态或策略对象的引用

四、享元模式的应用场景

享元模式在以下场景中特别有用:

4.1 图形编辑器

在图形编辑器中,字符、图形等对象可能有大量重复实例。例如:

  • 每个字符的字形(内部状态)可以被共享

  • 字符的位置、颜色等(外部状态)由外部维护

4.2 游戏开发

游戏中经常需要创建大量相似对象:

  • 粒子系统中的粒子

  • 地图中的树木、建筑等重复元素

  • 同类型的NPC或敌人

4.3 数据库连接池

连接池是享元模式的典型应用:

  • 连接对象被创建后放入池中

  • 客户端从池中获取连接而不是新建

  • 使用完毕后归还到池中

4.4 其他应用

  • 文本处理中的字符串池

  • 浏览器中的DOM节点复用

  • 财务系统中的共享会计科目对象

五、享元模式的优缺点

5.1 优点

  1. 大幅减少内存使用:通过共享相同内部状态的对象,显著降低内存消耗

  2. 提高性能:减少了对象创建和垃圾回收的开销

  3. 集中管理共享状态:所有共享状态集中存储,便于管理和维护

  4. 外部状态独立:外部状态的变化不会影响共享的内部状态

5.2 缺点

  1. 增加系统复杂性:需要区分内部状态和外部状态,增加了设计难度

  2. 可能引入线程安全问题:如果外部状态处理不当,可能导致并发问题

  3. 查找开销:维护共享对象池可能需要额外的查找开销

  4. 不适用于所有场景:当对象间差异很大时,享元模式可能不适用

六、实际案例分析

6.1 Java String常量池

Java中的String类使用了享元模式的思想。字符串常量池(String Pool)是享元模式的经典实现:

String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");System.out.println(s1 == s2); // true,引用同一个对象
System.out.println(s1 == s3); // false,s3是新创建的对象

JVM会维护一个字符串常量池,相同的字符串字面量会指向池中的同一个对象。

6.2 浏览器中的DOM渲染

现代浏览器在渲染DOM时也使用了享元模式的思想:

  • 相同类型的DOM节点可以共享相同的样式计算

  • 只有位置、内容等外部状态需要单独存储

  • 这大大提高了页面渲染性能

6.3 棋牌游戏开发

以麻将游戏为例:

// 麻将牌享元工厂
class MahjongTileFactory {private static Map<String, MahjongTile> tiles = new HashMap<>();static {String[] types = {"万", "条", "筒"};for (String type : types) {for (int i = 1; i <= 9; i++) {tiles.put(type + i, new MahjongTile(type, i));}}}public static MahjongTile getTile(String type, int num) {return tiles.get(type + num);}
}// 客户端
public class MahjongGame {public static void main(String[] args) {// 玩家手牌List<MahjongTile> handTiles = new ArrayList<>();// 添加牌,相同牌号的牌会共享同一个对象handTiles.add(MahjongTileFactory.getTile("万", 1));handTiles.add(MahjongTileFactory.getTile("条", 5));handTiles.add(MahjongTileFactory.getTile("万", 1)); // 与第一个是同一个对象System.out.println("手牌数量: " + handTiles.size());System.out.println("实际创建的牌对象数量: " + MahjongTileFactory.getTileCount());}
}

在这个例子中,相同牌号的麻将牌共享同一个对象,大大减少了内存使用。

七、享元模式的最佳实践

  1. 合理划分内部和外部状态:这是享元模式成功应用的关键

  2. 使用工厂管理享元对象:集中管理可以确保正确共享

  3. 考虑线程安全性:特别是在多线程环境中

  4. 权衡性能与内存:不是所有情况都适合使用享元模式

  5. 结合其他模式使用:如工厂模式、组合模式等

八、总结

享元模式是一种通过共享技术来支持大量细粒度对象的高效设计模式。它通过区分内部状态和外部状态,使得具有相同内部状态的对象可以被共享,从而显著减少内存消耗和提高系统性能。正确应用享元模式需要对业务场景有深入理解,能够准确识别可共享的内部状态和不可共享的外部状态。

虽然享元模式在特定场景下非常有效,但它并非银弹。开发者需要根据实际情况权衡利弊,决定是否采用享元模式。当系统中存在大量相似对象且内存是瓶颈时,享元模式无疑是一个强大的工具;但当对象间差异很大或外部状态过于复杂时,可能需要考虑其他解决方案。

理解并掌握享元模式,能够帮助开发者设计出更加高效、优雅的软件系统,特别是在资源受限的环境中。

Java中的String类使用了享元模式的思想。字符串常量池(String Pool)是享元模式的经典实现:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/915159.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/915159.shtml
英文地址,请注明出处:http://en.pswp.cn/news/915159.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络大提速,RDMA,IB,iWrap

本章第一节介绍的存储设备方面的创新解决了CPU访问存储设备的性能问题。但在实际的业务当中,数据的传输除了在节点内部的CPU与存储设备间外,节点之间也存在数据传输的需求。本节我们就介绍在网络传输方面是如何提速的。 在介绍新的网络技术之前,我们看看传统网络是如何传输…

【C++】红黑树,“红“与“黑”的较量

各位大佬好&#xff0c;我是落羽&#xff01;一个坚持不断学习进步的大学生。 如果您觉得我的文章有所帮助&#xff0c;欢迎多多互三分享交流&#xff0c;一起学习进步&#xff01; 也欢迎关注我的blog主页: 落羽的落羽 一、红黑树的概念与规则 红黑树是一种更加特殊的平衡二…

【愚公系列】《MIoT.VC》001-认识、安装 MIoT.VC 软件

💎【行业认证权威头衔】 ✔ 华为云天团核心成员:特约编辑/云享专家/开发者专家/产品云测专家 ✔ 开发者社区全满贯:CSDN博客&商业化双料专家/阿里云签约作者/腾讯云内容共创官/掘金&亚马逊&51CTO顶级博主 ✔ 技术生态共建先锋:横跨鸿蒙、云计算、AI等前沿领域…

git:tag标签远程管理

git tag v1&#xff1a;在当前所在分支创建标签v1git tag -a v2 -m release version&#xff1a;创建一个带有附注的标签git tag -d v2&#xff1a;删除本地标签git tag&#xff1a;查看标签git push origin 标签1 标签2……&#xff1a;把多个标签推送到远程git push origin -…

力扣 hot100 Day49

105. 从前序与中序遍历序列构造二叉树 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 //抄的 class Solution { private:unordered_map<int, i…

jvm-sandbox-repeater 录制和回放

https://github.com/alibaba/jvm-sandbox-repeater/blob/master/docs/user-guide-cn.md 快速录制自己应用 step0 安装sandbox和插件到应用服务器 curl -s https://github.com/alibaba/jvm-sandbox-repeater/releases/download/v1.0.0/install-repeater.sh | sh step1 修改repe…

【C++底层剖析】++a vs a++:到底谁是左值,谁是右值?

在 C 编程中&#xff0c;我们经常使用 a 和 a 来实现自增操作。乍一看它们只是“先加还是后加”的语法糖&#xff0c;但你真的理解它们的底层机制、返回值类型和左值右值属性吗&#xff1f;1. a 和 a 的基础区别表达式名称语义返回值类型左值 / 右值a前置自增先将 a 加 1&#…

【世纪龙科技】汽车故障诊断与排除仿真教学软件让课堂更高效安全

随着汽车产业向智能化、电动化快速转型&#xff0c;职业院校汽修专业的教学模式正面临全新挑战。传统实车实训存在成本高、风险大、场景单一等问题&#xff0c;而行业对人才的要求却越来越高——既需要扎实的理论基础&#xff0c;又必须具备熟练的故障诊断能力。如何在保证安全…

网络基础9:按流负载均衡实验(等价路由)

实验eNS拓扑图&#xff1a;1. 网络拓扑与 IP 配置AR5&#xff1a;GE0/0/0: 192.168.1.1/24&#xff08;连接 AR6&#xff09;GE0/0/1: 192.168.3.1/24&#xff08;连接 AR8&#xff09;Loopback0: 1.1.1.1/32&#xff08;源地址&#xff09;AR6&#xff1a;GE0/0/0: 192.168.1.…

4G模块 A7680发送中文短信到手机

命令说明 基础AT指令 ATi显示产品的标志信息 ATCIMI查询IMSI ATCICCID从SIM卡读取ICCID ATCGSN查询产品序列号 ATCPIN查询卡状态 ATCSQ查询信号强度 ATCGATT查询当前PS域状态 ATCREG查询GPRS注册状态 ATCEREG查询4G注册状态 ATCGPADDR查询PDP地址 ATCMGF选择短信格式 ATCMGS发…

深度学习-线性神经网络

文章目录线性回归基本概念随机梯度下降矢量化加速正态分布和平方损失极大似然估计线性回归实现从0开始**torch.no_grad()的两种用途****为什么需要 l.sum().backward()&#xff1f;**调用现成库softmax回归图像数据集从0开始实现softmax利用框架API实现课程学习自李牧老师B站的…

【王树森推荐系统】推荐系统涨指标的方法04:多样性

涨指标的方法有哪些&#xff1f; 改进召回模型&#xff0c;添加新的召回模型改进粗排和精排模型提升召回&#xff0c;粗排&#xff0c;精排的多样性特殊对待新用户吗&#xff0c;低活用户等特殊人群利用关注&#xff0c;转发&#xff0c;评论这三种交互行为 排序的多样性 精排多…

1. Spring AI概述

一、前言 Spring AI 是由 Spring 团队推出的开源项目&#xff0c;旨在为 Java 开发者提供简洁、一致的 Spring 风格开发体验&#xff0c;用于构建基于生成式人工智能&#xff08;GenAI&#xff09;和大型语言模型&#xff08;LLM&#xff09;的应用程序。它通过标准化抽象层简…

[每日随题10] DP - 重链剖分 - 状压DP

整体概述 难度&#xff1a;1600 →\rightarrow→ 2200 →\rightarrow→ 2600 P6005 [USACO20JAN] Time is Mooney G 标签&#xff1a;DP 前置知识&#xff1a;链式前向星 难度&#xff1a;绿 1600 题目描述&#xff1a; 输入格式&#xff1a; 输出格式&#xff1a; 样例输…

【Ubuntu22.04】repo安装方法

背景 repo是Google开发的用于基于git管理Android版本库的一个工具&#xff0c;管理多个Git仓库的工具&#xff0c;它可以帮助您在一个代码库中管理多个Git仓库的代码。其在鸿蒙操作系统中大量使用。下面我们就介绍repo在wsl中的安装部署。 安装方法 使用中国科技大学资源 脚本i…

Vue3的definePros和defineEmits

在 Vue 3 中&#xff0c;defineProps 和 defineEmits 是组合式 API 中用于定义组件的 props 和 事件 的方法&#xff0c;提供了一种更简洁和明确的方式来管理组件的输入和输出。它们属于 Composition API 的一部分&#xff0c;在 Vue 2 中通常使用 props 和 $emit 来实现。1. d…

【华为机试】122. 买卖股票的最佳时机 II

文章目录122. 买卖股票的最佳时机 II描述示例 1示例 2示例 3提示解题思路核心观察关键洞察算法实现方法1&#xff1a;贪心算法&#xff08;推荐&#xff09;方法2&#xff1a;动态规划方法3&#xff1a;动态规划&#xff08;空间优化&#xff09;方法4&#xff1a;波峰波谷法算…

Spring MVC @RequestParam注解全解析

RequestParam 注解详解 RequestParam 是 Spring MVC 中最常用的注解之一&#xff0c;用于从 HTTP 请求中提取查询参数&#xff08;Query String&#xff09;或表单数据。它主要处理 application/x-www-form-urlencoded 类型的请求&#xff08;如 GET 请求或 POST 表单提交&…

从零掌握XML与DTD实体:原理、XXE漏洞攻防

本文仅用于技术研究&#xff0c;禁止用于非法用途。 Author:枷锁 文章目录一、XML基础1. 什么是XML&#xff1f;2. XML语法规则3. 数据类型二、DTD1. 认识DTD2. 声明DTD3. DTD实体4. 如何防御XXE攻击&#xff1f;5. 总结一、XML基础 1. 什么是XML&#xff1f; XML &#xff1…

.NET 8 Release Candidate 1 (RC1)现已发布,包括许多针对ASP.NET Core的重要改进!

.NET 8 Release Candidate 1 (RC1)发布&#xff1a;ASP.NET Core重大改进来袭&#xff01; 近日&#xff0c;.NET 8 Release Candidate 1 (RC1)正式发布&#xff0c;这是在今年晚些时候计划发布的最终 .NET 8 版本之前的两个候选版本中的第一个。此版本包含了大部分计划中的功…