定義
享元(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將其從永久代拿出來放置於堆中。