定义
享元(Flyweight)模式又称蝇量模式,主要运用共享技术来有效地支持大量细粒度对象的复用。主要用于减少创建对象的数量,以减少内存占用和提高性能。
如果想要让某个类的一个实例用来提供许多 “虚拟实例” ,就可以考虑使用享元模式。
享元模式属于对象结构型模式。
要点
优点:
- 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。
- 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
缺点:
- 一旦使用享元模式,那么单个的逻辑实例将无法拥有独立而不同的行为。
- 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
- 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。
享元模式的主要角色有:
抽象享元(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
具体享元 (Concrete Flyweight):实现抽象享元角色中所规定的接口。
非享元(Unsharable Flyweight):是不可以共享的外部状态,它以参数的形式注入具体享元中。
享元工厂(Flyweight Factory):负责创建和管理享元角色。它提供一个用于存储享元对象的享元池,当用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
场景
某公司需要开发一款内网网盘,该网盘对于相同的文件只保留一份,譬如当上传一部别人上传过的文件,即使修改了文件名,只要文件内容一致,会发现很快就上传完成了,实际上不是真的上传,而是引用别人曾经上传过的那份文件,不但可以提高用户体验,还可以节约存储空间避免资源浪费。
实现
Resource(享元内部状态)
/**
* 资源类 Resource,相当于享元类的内部状态
*/
public class Resource {
/**
* 文件资源唯一ID
*/
private String hashId;
/**
* 文件大小
*/
private long byteSize;
/**
* 文件内容
*/
private byte[] content;
public Resource(String hashId, long byteSize, byte[] content) {
this.hashId = hashId;
this.byteSize = byteSize;
this.content = content;
}
public String getHashId() {
return hashId;
}
public long getByteSize() {
return byteSize;
}
public byte[] getContent() {
return content;
}
@Override
public String toString() {
return "{ 资源ID=" + hashId +
", 文件大小=" + byteSize +
" bytes }";
}
}
UserFile(享元类)
/**
* 用户的文件类,相当于具体享元类(ConcreteFlyweight)
*
* 其中的 resource 为内部状态
* owner 和 filename为外部状态,也就是非享元(UnsharedConcreteFlyWeight),外部状态不可共享
*/
public class UserFile {
/**
* 文件名
*/
private String filename;
/**
* 用户
*/
private String owner;
/**
* 文件资源信息
*/
private Resource resource;
public UserFile(String owner, String filename, Resource resource) {
this.owner = owner;
this.filename = filename;
this.resource = resource;
}
public String getFilename() {
return filename;
}
public String getOwner() {
return owner;
}
public Resource getResource() {
return resource;
}
@Override
public String toString() {
return "" +
"文件名:" + filename +
", 用户:" + owner +
", 资源信息:" + resource;
}
}
PanServer(享元工厂)
/**
* 网盘服务,相当于享元工厂(FlyWeightFactory)
*/
public class PanServer {
/**
* 单例模式 - 网盘服务
*/
private static PanServer server = new PanServer();
/**
* 文件资源池,相当于享元池,只保留不同的文件资源 (hashId : Resource)
*/
private Map<String, Resource> resourceSystem;
/**
* 用户文件列表 (filename : UserFile)
*/
private Map<String, UserFile> fileSystem;
/**
* 文件上传位置
*/
private final String DEST_FILEPATH = Thread.currentThread().getContextClassLoader().getResource("").getPath() + "com/codedancing/designpattern/structural/flyweight/files/";
private PanServer() {
resourceSystem = new HashMap<>();
fileSystem = new HashMap<>();
}
public static PanServer getInstance() {
return server;
}
/**
* 上传文件
*/
public void upload(String username, String fileName, String filePath) {
long startTime = System.currentTimeMillis();
System.out.println("准备上传文件: " + fileName);
File source = new File(filePath);
File dest = new File(DEST_FILEPATH + fileName);
if (dest.exists()) {
System.out.println("文件名称已存在,请重新上传\n\n");
return;
}
// 检测文件,利用文件内容计算文件的唯一ID
String hashId = HashUtil.computeHashId(ReadFileUtil.readFileToBytes(filePath));
try {
if (resourceSystem.containsKey(hashId)) {
System.out.println(String.format("Server:检测到内容相同的文件 [ %s ] ,为了节约空间,重用文件", fileName));
// 取出资源池中重复的文件资源
Resource resource = resourceSystem.get(hashId);
// 保存新文件至用户文件
fileSystem.put(fileName, new UserFile(username, fileName, resource));
Thread.sleep(100);
} else {
System.out.println("正在上传文件: " + fileName + " ...");
// 开始上传
uploadFileToServer(source, dest);
// 添加文件资源到资源池
Resource resource = new Resource(hashId, dest.length(), ReadFileUtil.readFileToBytes(DEST_FILEPATH + fileName));
resourceSystem.put(hashId, resource);
fileSystem.put(fileName, new UserFile(username, fileName, resource));
// 上传文件需要耗费一定时间
Thread.sleep(3000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println(String.format("文件上传完成,共耗费 %s 毫秒\n\n", endTime - startTime));
}
/**
* 上传指定文件到指定目录
*/
private void uploadFileToServer(File source, File dest) {
FileChannel inputChannel = null;
FileChannel outputChannel = null;
try {
inputChannel = new FileInputStream(source).getChannel();
outputChannel = new FileOutputStream(dest).getChannel();
outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (inputChannel != null) {
inputChannel.close();
}
if (outputChannel != null) {
outputChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 下载文件(模拟)
*/
public void download(String fileName) {
UserFile file = fileSystem.get(fileName);
if (file == null) {
System.out.println("文件不存在");
} else {
System.out.println("下载文件 --- " + file);
}
}
}
Client
public class Client {
public static void main(String[] args) {
PanServer server = PanServer.getInstance();
// 上传文件
System.out.println("=================上传文件===================");
String filePath = "/home/codedancing/Desktop/HeadFirst设计模式.pdf";
String filePath1 = "/home/codedancing/Desktop/朱一旦的HeadFirst设计模式.pdf";
String filePath2 = "/home/codedancing/Desktop/Test-1.0-SNAPSHOT.war";
server.upload("朱一旦", "HeadFirst设计模式.pdf", filePath);
server.upload("朱一旦", "HeadFirst设计模式.pdf", filePath);
server.upload("朱一旦", "朱一旦的HeadFirst设计模式.pdf", filePath1);
server.upload("朱一旦", "Test-1.0-SNAPSHOT.war", filePath2);
// 下载文件
System.out.println("=================下载文件===================");
server.download("HeadFirst设计模式.pdf");
server.download("HeadFirst设计模式.pdf");
server.download("朱一旦的HeadFirst设计模式.pdf");
server.download("Test-1.0-SNAPSHOT.war");
}
}
-------------------输出---------------------
=================上传文件===================
准备上传文件: HeadFirst设计模式.pdf
正在上传文件: HeadFirst设计模式.pdf ...
文件上传完成,共耗费 3033 毫秒
准备上传文件: HeadFirst设计模式.pdf
文件名称已存在,请重新上传
准备上传文件: 朱一旦的HeadFirst设计模式.pdf
Server:检测到内容相同的文件 [ 朱一旦的HeadFirst设计模式.pdf ] ,为了节约空间,重用文件
文件上传完成,共耗费 103 毫秒
准备上传文件: Test-1.0-SNAPSHOT.war
正在上传文件: Test-1.0-SNAPSHOT.war ...
文件上传完成,共耗费 3049 毫秒
=================下载文件===================
下载文件 --- 文件名:HeadFirst设计模式.pdf, 用户:朱一旦, 资源信息:{ 资源ID=1696fa43526aea538cce93a98163e7cb, 文件大小=149023 bytes }
下载文件 --- 文件名:HeadFirst设计模式.pdf, 用户:朱一旦, 资源信息:{ 资源ID=1696fa43526aea538cce93a98163e7cb, 文件大小=149023 bytes }
下载文件 --- 文件名:朱一旦的HeadFirst设计模式.pdf, 用户:朱一旦, 资源信息:{ 资源ID=1696fa43526aea538cce93a98163e7cb, 文件大小=149023 bytes }
下载文件 --- 文件名:Test-1.0-SNAPSHOT.war, 用户:朱一旦, 资源信息:{ 资源ID=b0e5be44bdbbe8705e873a13504b958d, 文件大小=5839999 bytes }
Process finished with exit code 0
源码
总结
- 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源,可以考虑使用享元模式。
- 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
- 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。
JDK中的String类使用了享元模式
Java中的字符串一般保存在字符串常量池中,java会确保一个字符串在常量池中只有一个拷贝,这个字符串常量池在JDK6.0以前是位于常量池中,位于永久代,而在JDK7.0中,JVM将其从永久代拿出来放置于堆中。