这个设计模式享元模式,听名字的意思就是共享的单元,但是他的英文名称挺有意思的叫做“flyweight”,蝇量级拳手这个也挺有意思的,实际就是做一个共享的模块,里面是我们常用的,而且可以帮助我们减少创建对象,更好的节省资源,把重量级的活缩减成蝇量级的活。
一 初步理解享元
具体的例子,其实就是线上棋牌游戏,假设有很多个房间,每个房间都要有一副牌,牌的数量,花色,大小也就那些,假设我们玩的是一副扑克牌,一副扑克牌应该是54张,咱们完玩的是斗地主。每个牌对应的是一下:
/**
* @Author: zhangpeng
* @Description:
* @Date: 2022/8/22
*/
public class Poke{
private Int pokerSuit;
private String pokerNumber;
public static enum pokerSuit{ RED, BLACK }
}
那一个房间就要new出54个对象,一万个房间就是540000,但是明显太占空间了,尤其是线上这种业务很容易内存爆了,影响业务影响体验。
那我们可不可以共享这么一副牌,类似一个枚举,大家都是这一副牌,我只需要记录手牌是什么,以及扔掉的牌的记录就好了。
二 一个简单的棋类享元demo
同理玩牌是这样,棋类游戏也是类似。我们这里有一个demo是关于棋的
注释的部分是改造前的
package com.example.test.flyweight.chess;
/**
* @Author: zhangpeng
* @Description: 享元模式---棋子
* @Date: 2022/8/22
*/
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 positionY) {
// this.id = id;
// this.text = text;
// this.color = color;
// this.positionX = positionX;
// this.positionY = positionX;
// }
//
// public static enum Color { RED, BLACK }
//
// // ...省略其他属性和getter/setter方法...}
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;
}
}
棋盘这也要对比改造前后的
package com.example.test.flyweight.chess;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: zhangpeng
* @Description: 享元模式---棋局
* @Date: 2022/8/22
*/
public class ChessBoard {
// 常规模式下的棋盘
// private Map 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) {
// //...省略...
// }
private Map 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) {
//...省略...
}
}
棋子的享元:
package com.example.test.flyweight.chess;
/**
* @Author: zhangpeng
* @Description:
* @Date: 2022/8/22
*/
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方法...
}
享元的工厂,这里缓存要用到的所有棋子
package com.example.test.flyweight.chess;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: zhangpeng
* @Description:
* @Date: 2022/8/22
*/
public class ChessPieceUnitFactory {
private static final Map 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);
}
}
大家具体代码可以看我的git提交,除了棋子的享元的damo还有一个是关于简单文本对于字体格式的享元demo(大家可以star一下这个库,基本我的设计模式demo和别的一些工具demo我都会放在这里)
享元demo
三 具体解析享元demo
享元重要的是一个共享的那部分是不能改变的。
他的使用场景就是当程序中存在大量相似对象,每个对象之间只是根据不同的使用场景有些许变化时,考虑对他们提取出来改造,节省空间。类似于有一个工厂来单独管理他们。
像我们网盘有的链接保存也是这样,你复制一个文本的链接像保存你的网盘里,文件很大,但是人家网盘几乎毫秒级别的相应,实际上人家没有做什么复制保存这些消耗IO的操作,人家就是把文件对应的地址保存一下,实际上这个地址你可以理解为一个享元。
还有我们demo当中所谓的一些缓存也是这么做,我直接缓存一些你常用的,等你用的时候,感觉“好快”。
借用一个别人的图说明一下享元模式的结构
Flyweight
享元接口,定义所有对象共享的操作
ConcreteFlyweight
具体的要被共享的对象,其一般是一个不可变类,内部只保存需要共享的内部状态,它可能不止一个。
FlyweightFactory
负责给客户端提供共享对象
以上的三部分对应我们的demo
下棋的这一步应该是享元接口单手i我这边没有体现,共享的对象ChessPieceUnit ,ChessPieceUnitFactory 提供共享对象的工厂
四 常用的缓存
大多数情况下,享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或者 List 来缓存已经创建好的享元对象,以达到复用的目的。
我们常见的享元模式可以从基本类型包装类种体会.
首先要明白一个自动拆箱,自动装箱的概念。很简单以下代码就是:
Integer i = 56; //自动装箱
int j = i; //自动拆箱
装箱就是一个基本类型直接转换成包装类,比如Integer i = 56。
但实际“自动”替你做了一步
#Integer i = 59;底层执行了:但是有没有展示出来
Integer i = Integer.valueOf(59);
同理拆箱,就是一个包装类转换成它对应的基本类型
#int j = i; 底层执行了:i是包装类的实例
int j = i.intValue();
了解上面这些,我们可以执行下面代码玩玩,但是在这之前可以先预估一下结果:
Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
结果是这样的:
有没有感觉到很奇怪的,按理来说应该是要不都是false,要不都是true怎么同样的操作,结果不一样,怎么JDK不注意“幂等性”吗?
不是的实际上,这个是包装类的优化,所谓的优化就是Integer类做了一个缓存。
在代码里面就是内部类IntegerCache
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
IntegerCache 只缓存 -128 到 127 之间的整型值,为什么不多弄点,要考虑你缓存太多也占空间,不如就一个字节大小,也就是2的7到-2的7次方-1(不太会打符号,将就看吧,理解就行)
同理别的数字类型的基础类也会有类似缓存。
String也有类似的样子,请看以下代码:
String str1 = "ZP";
String str2 = "ZP";
String str3 = new String("zp");
System.out.println(str1 == str2);
System.out.println(str1 == str3);
至于解释,我直接抄小争哥的对应设计模式的图就可以
这么一看,实际享元模式在我们日常的代码源码中使用的比较广泛,包括我们自己写框架,写到涉及到缓存的部分基本都会有这个模式,但是这个也要跟创建模型的单例区别开,虽然都是不可变的,但是使用场景和目的不一样,享元模式是结构型的设计模式。