享元模式

定义

享元(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

源码

Click here


总结
  1. 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源,可以考虑使用享元模式。
  2. 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
  3. 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。

JDK中的String类使用了享元模式
Java中的字符串一般保存在字符串常量池中,java会确保一个字符串在常量池中只有一个拷贝,这个字符串常量池在JDK6.0以前是位于常量池中,位于永久代,而在JDK7.0中,JVM将其从永久代拿出来放置于堆中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值