设计模式:享元模式,如何利用享元模式优化内存占用

享元模式的原理与实现

  • 所谓”享元“,就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元模式是不可变的对象。

  • 具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。

  • 定义中的“不可变对象”指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或属性)就不会再被修改了。所以,不可变对象不能暴露任何set()等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。

举个例子。

假设我们在开发一个棋牌游戏(比如象棋)。一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局中要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。具体代码如下。其中,ChessPiece 类表示棋子,ChessBoard 类表示一个棋局,里面保存了象棋中 30 个棋子的信息。

public class ChessPiece {//棋子
	private int id;
	private String text;
	private Color color;
	private int positionX;
	private int positionY;
	
	public ChessPiece(int id, String text, Color color, int positionX, int positi
		this.id = id;
		this.text = text;
		this.color = color;
		this.positionX = positionX;
		this.positionY = positionX;
	}
	
	public static enum Color {
	 	RED, BLACK
	 }
	 // ...省略其他属性和getter/setter方法...
}

public class ChessBoard {//棋局
	 private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
	 
	 public ChessBoard() {
	 	init();
	 }
	 
	 private void init() {
		 chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 0));
		 chessPieces.put(2, new ChessPiece(2,"馬", ChessPiece.Color.BLACK, 0, 1));
		 //...省略摆放其他棋子的代码...
	 }
	 
	 public void move(int chessPieceId, int toPositionX, int toPositionY) {
		 //...省略...
	 }
}

为了记录每个房间当前的棋局情况,我们需要给每个房间都场景一个ChessBoard棋局对象。因为游戏大厅中有成千上万的房间(实际上,百万人同时在线的游戏大厅也有很多),那保存这么多棋局对象就会消耗大量的内存。有没有什么办法来节省内存呢?

这个时候,享元模式就可以派上用场了。向刚刚的实现方式,在内存中会有大量的相似对象。这些相似对象的id、text、color都是相同的,唯独positionX、positionY不同。实际上,我们可以将棋子的id、text、color属性拆分出来,设计成单独的类,并且作为享元供多个棋盘复用。这样,棋盘只需要记录每个棋子的位置信息就可以了。如下:

// 享元类
public class ChessPieceUnit {
	 private int id;
	 private String text;
	 private Color color;
	 
	 public ChessPieceUnit(int id, String text, Color color) {
		 this.id = id;
		 this.text = text;
		 this.color = color;
	}
	
	public static enum Color {
		RED, BLACK
	}
	// ...省略其他属性和getter方法...
}


public class ChessPieceUnitFactory {
	private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();
	
	static {
		pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
		pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
		//...省略摆放其他棋子的代码...
	}
	
	public static ChessPieceUnit getChessPiece(int chessPieceId) {
		return pieces.get(chessPieceId);
	}
}
public class ChessPiece {
	private ChessPieceUnit chessPieceUnit;
	private int positionX;
	private int positionY;
	
	public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
		this.chessPieceUnit = unit;
		this.positionX = positionX;
		this.positionY = positionY;
	}
	// 省略getter、setter方法
}

public class ChessBoard {
	private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
	
	public ChessBoard() {
		init();
	}

	private void init() {
		chessPieces.put(1, new ChessPiece(
		ChessPieceUnitFactory.getChessPiece(1), 0,0));
		chessPieces.put(1, new ChessPiece(
		ChessPieceUnitFactory.getChessPiece(2), 1,0));
		//...省略摆放其他棋子的代码...
	}
	
	public void move(int chessPieceId, int toPositionX, int toPositionY) {
	 //...省略...
	 }
}

在上面的代码实现中,我们利用工厂类来缓存ChessPieceUnit信息(也就是id、text、color)。通过工厂类获取到ChessPieceUnit就是享元。所有的ChessBoard对象共享者30个ChessPieceUnit对象(因为象棋中只有30个棋子)。在使用享元模式之前,记录1万个棋局,我们要创建30万(30*1万)个棋子的ChessPieceUnit 对象。利用享元模式,我们只需要创建30个享元对象供所有棋局共享使用即可,大大节省了内存。

那享元模式的原理讲完了,我们来总结一下它的代码结构。实际上,它的代码实现非常简单,主要通过工厂模式,在工厂类中,通过一个Map来缓存已经创建过的享元对象,来达到复用的目的

享元模式在文本编辑器中的应用

可以把这里提到的文本编辑器想象成 Office 的 Word。不过,为了简化需求背景,我们假设这里的文本编辑器只实现了文本编辑功能,不包含图片、表格等复杂的编辑功能。对于简化之后的文本编辑器,我们要在内存中表示一个文本文件,只需要记录文字和格式两部分信息就可以了,其中,格式又包括文字的字体、大小、颜色等信息。

尽管在实际的文档编写中,我们一般都是按照文本类型(标题、正文…)来设置文字的格式,标题一般是一种格式,正文是另一种格式等等。但是,从理论上来讲,我们可以给文本文件中的每个文字都设置不同的格式。为了实现如此灵魂的格式设置,并且代码实现又不过于太复杂,我们把每个文字都当作一个独立的对象来看待,并且在其中包含它的格式信息。具体代码如下:

public class Character {//文字
	private char c;
	private Font font;private int size;
	private int colorRGB;
	public Character(char c, Font font, int size, int colorRGB) {
		this.c = c;
		this.font = font;
		this.size = size;
		this.colorRGB = colorRGB;
	}
}

public class Editor {
	private List<Character> chars = new ArrayList<>();
	
	public void appendCharacter(char c, Font font, int size, int colorRGB) {
		Character character = new Character(c, font, size, colorRGB);
		chars.add(character);
	}
}

在文本编译器中,我们每敲一个文字,都会调用Editor类中的appendCharacter()方法,创建一个新的Character对象,保存到chars数组中。如果一个文本文件中,有上万、十几万、几十万的文字,那我们就要在内存中存储这么多的Character对象。那有没有办法可以节省一点内存呢?

实际上,在一个文本文件中,用到的字体格式不会太多,毕竟不太可能有人把每个文字都设置成不同的格式。所以,对于字体格式,我们可以将他设计成享元,让不同的文字共享使用。如下:

public class CharacterStyle {
	private Font font;
	private int size;
	private int colorRGB;
	
	public CharacterStyle(Font font, int size, int colorRGB) {
		this.font = font;
		this.size = size;
		this.colorRGB = colorRGB;
	}
	
	@Override
	public boolean equals(Object o) {
		CharacterStyle otherStyle = (CharacterStyle) o;
		
		return font.equals(otherStyle.font)
		&& size == otherStyle.size
		&& colorRGB == otherStyle.colorRGB;
	}
}

public class CharacterStyleFactory {
	private static final List<CharacterStyle> styles = new ArrayList<>();
	
	public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
		CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
		for (CharacterStyle style : styles) {
			if (style.equals(newStyle)) {
				return style;
			}
		}
		styles.add(newStyle);
		return newStyle;
	}
}

public class Character {
	private char c;
	private CharacterStyle style;
	
	public Character(char c, CharacterStyle style) {
		this.c = c;
		this.style = style;
	}
}
public class Editor {
	private List<Character> chars = new ArrayList<>();
	
	public void appendCharacter(char c, Font font, int size, int colorRGB) {
		Character character = new Character(c, CharacterStyleFactory.getStyle(font
		chars.add(character);
	}
}

享元模式 vs 单例、缓存、对象池

在上面的讲解中,我们多次提到“共享”“缓存”“复用”这些字眼,那它跟单例、缓存、对象池这些概念有什么区别呢?我们来简单对比一下。

(1)享元模式 VS 单例模式

  • 在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于单例的变体:多利
  • 但是,区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。
    • 享元模式是为了对象复用,节省内存
    • 多例模式是为了限制对象的个数

(2)享元模式 VS 缓存

  • 在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。
  • 这里的“缓存实际上是”存储“的意思,跟我们平时说的”数据库缓存“、”CPU缓存“等是两回事。我们平时说的缓存,主要是为了提高访问效率,而非复用

(3)享元模式 VS 对象池

  • 对象池、连接池,线程池等也是为了复用,那它们跟享元模式有什么区别呢?
  • 这里重点提一下对象池。向C++这样的编程语言,内存的管理是由程序员负责的。为了避免频繁的进行对象的创建和释放导致内存碎片,我们可以预先申请一片连续的内存空间,也就是这里说的对象池。每次创建对象时,我们从对象池中直接取出一个空闲的对象来使用,对象使用完成之后,再放回到对象池中以供后继复用,而非直接释放掉
  • 虽然对象池、连接池、线程池、享元模式都是为了复用,但是,如果我们再细致的抠一抠“复用”这个字眼的话,对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。
  • 池化技术中的”复用“可以理解为”重复使用“,主要目的时节省时间(比如冲数据库连接池中取一个连接,不需要重复创建)。再任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的”复用“可以理解为”共享使用“,在整个生命周期中,都是被所有使用者共享的,主要目的时节省空间

总结

(1)享元模式的原理

  • 所谓”享元“,就是被共享的单元。
  • 享元模式的意图时复用对象,节省内存,前提时享元对象是不可变对象。
  • 具体来讲,当一个系统中存在大量重复对象的时候,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。
  • 实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段),提取出来设计成享元,让这些大量相似的对象引用这些享元

(2)享元模式的代码实现

  • 主要是通过工厂模式,在工厂类中,通过一个Map或者List来缓存已经创建好的对象,以达到复用的目的

(3)享元模式 VS 单例、缓存、对象池

  • 区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。这里的区别也不例外
    • 应用单例模式是为了保证对象全局唯一。
    • 应用享元模式是为了保证对象复用,节省内存
    • 缓存是为了提高访问效率,而非复用
    • 池化技术中的”复用“理解为”重复使用“,主要是为了节省时间
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值