【设计模式系列13】今天才知道,原来我一直在用享元模式

前言

我们知道,数据库的连接非常消耗性能,所以就有了连接池来减少连接操作的性能消耗;
如果一个系统中需要创建大量线程,也会消耗大量性能,所以就有了线程池。而我们在面向对象过程中,会创建大量对象,而如果有些对象可以被重复使用,那么我们是不是可以创建一个对象池来减少创建对象所带来的性能消耗呢?

答案是肯定的,这就是我们今天需要学习的享元模式做的事情。

什么是享元模式

享元模式(Flyweight Pattern),又称之为轻量级模式,是对象池的一种实现。主要用于减少创建对象的数量,以减少内存占用和提高性能。类似于我们的数据库连接池和线程池。

享元模式的宗旨就是共享细粒度对象,将多个对同一对象的访问集中起来,不必为每个访问者创建一个单独的对象,以此来降低内存的消耗。

享元模式属于结构型模式。

光讲理论不写代码的都是耍流氓,所以老规矩:Talk is cheap,Show you the code

享元模式示例

我们以买火车票为例子来进行示例。

1、首先创建一个车票的享元接口,定义一个查询车票信息方法:

package com.zwx.design.pattern.flyweight;

/**
 * 抽象享元角色
 */
public interface ITicket {
    
    
    void info();
}

2、然后定义一个实现类来实现ITicket 接口:

package com.zwx.design.pattern.flyweight;

/**
 * 具体享元角色(粗粒度)
 */
public class TrainTicket implements ITicket{
    
    
    private String from;
    private String to;

    public TrainTicket(String from, String to) {
    
    
        this.from = from;
        this.to = to;
    }

    @Override
    public void info() {
    
    
        System.out.println(from + "->" + to + ":硬座:100元,硬卧:200元");
    }
}

3、定义一个工厂类来管理享元对象:

package com.zwx.design.pattern.flyweight;

import java.util.HashMap;
import java.util.Map;

/**
 * 享元对象工厂
 */
public class TicketFactory {
    
    
    private static Map<String,ITicket> CACHE_POOL = new HashMap<>();

    public static ITicket getTicketInfo(String from,String to){
    
    
        String key = from + "->" + to;
        if (TicketFactory.CACHE_POOL.containsKey(key)){
    
    
            System.out.println("使用缓存");
            return TicketFactory.CACHE_POOL.get(key);
        }
        System.out.println("未使用缓存");
        ITicket ticket = new TrainTicket(from,to);
        CACHE_POOL.put(key,ticket);
        return ticket;
    }
}

工厂类主要使用了一个Map来存储对象,把火车票的出发地和目的地作为key值,如果存在了则直接从Map取,否则就新创建一个对象,并且加入到Map中。

4、最后写一个测试类来测试一下:

package com.zwx.design.pattern.flyweight;

public class TestTicket {
    
    
    public static void main(String[] args) {
    
    
        ITicket ticket = TicketFactory.getTicketInfo("深圳","广州");
        ticket.info();//首次创建对象
        ticket = TicketFactory.getTicketInfo("深圳","广州");
        ticket.info();//使用缓存
        ticket = TicketFactory.getTicketInfo("深圳","北京");
        ticket.info();//使用缓存
    }
}

输出结果为:

未使用缓存
深圳->广州:硬座:100元,硬卧:200元
使用缓存
深圳->广州:硬座:100元,硬卧:200元
未使用缓存
深圳->广州:硬座:100元,硬卧:200

可以看到,深圳->广州的车票,第二次查询时使用了缓存。

看到这个写法,可能有人要有疑问了,这不就是容器式单例模式的写法吗?是的,这其实就是一种容器式单例模式的写法,但是它和单例模式的关注点不一样,单例模式关注的是整个类只能存在一个实例,而享元模式关注的是实例对象,对于可以共享状态的实例对象实现缓存(唯一)。

上面的写法也有一个缺陷,比如车票价钱是写死的,假如我们只想查硬座,或者只想查硬卧呢,这又该如何实现呢?这就要涉及到享元模式的粒度划分了

享元模式状态

上面的例子中,我们可以把实例对象划分一下,比如上面车票的对象,我们可以把from和to两个属性作为可共享状态,不可改变。然后再新增一个属性用来对应座位。这就是享元模式的内部状态外部状态

内部状态

内部状态指对象共享出来的信息,存储在享元对象内部并且不会随环境的改变而改变

外部状态

外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。

请看下面示例2就是根据内部状态和外部状态改写之后的例子:

享元模式示例2

1、首先创建一个车票的享元接口,定义一个查询车票信息方法和一个设置座位的方法:

package com.zwx.design.pattern.flyweight;

/**
 * 抽象享元角色
 */
public interface IShareTicket {
    
    
    void info();
    void setSeat(String seatType);
}

2、然后定义一个实现类来实现IShareTicket 接口(当然,这里的价钱还是写死的,示例主要是用来体会设计模式的思想):

package com.zwx.design.pattern.flyweight;

import java.math.BigDecimal;

/**
 * 具体享元角色(细粒度)
 */
public class TrainShareTicket implements IShareTicket {
    
    
    private String from;//内部状态
    private String to;//内部状态

    private String seatType = "站票";//外部状态

    public TrainShareTicket(String from, String to) {
    
    
        this.from = from;
        this.to = to;
    }

    @Override
    public void setSeat(String seatType){
    
    
        this.seatType = seatType;
    }

    @Override
    public void info() {
    
    
        System.out.println(from + "->" + to + ":" + seatType + this.getPrice(seatType));
    }

    private BigDecimal getPrice(String seatType){
    
    
        BigDecimal value = null;
        switch (seatType){
    
    
            case "硬座":
                value = new BigDecimal("100");
                break;
            case "硬卧":
                value = new BigDecimal("200");
                break;
            default:
                value = new BigDecimal("50");
        }
        return value;
    }
}

这个相比较于上面的示例,多了一个seatType属性,也多了一个设置座位的对外方法。

3、定义一个工厂类来管理享元对象:

package com.zwx.design.pattern.flyweight;

import java.util.HashMap;
import java.util.Map;

/**
 * 享元对象工厂
 */
public class TicketShareFactory {
    
    
    private static Map<String,IShareTicket> CACHE_POOL = new HashMap<>();

    public static IShareTicket getTicketInfo(String from,String to){
    
    
        String key = from + "->" + to;
        if (TicketShareFactory.CACHE_POOL.containsKey(key)){
    
    
            System.out.println("使用缓存");
            return TicketShareFactory.CACHE_POOL.get(key);
        }
        System.out.println("未使用缓存");
        IShareTicket ticket = new TrainShareTicket(from,to);
        CACHE_POOL.put(key,ticket);
        return ticket;
    }
}

4、最后写一个测试类来测试一下:

package com.zwx.design.pattern.flyweight;

public class TestShareTicket {
    
    

    public static void main(String[] args) {
    
    
        IShareTicket ticket = TicketShareFactory.getTicketInfo("深圳","广州");
        ticket.setSeat("硬座");
        ticket.info();//首次创建对象
        ticket = TicketShareFactory.getTicketInfo("深圳","广州");
        ticket.setSeat("硬卧");
        ticket.info();//外部状态改变了,但是内部状态共享,依然可以使用缓存
    }
}

输出结果:

未使用缓存
深圳->广州:硬座100
使用缓存
深圳->广州:硬卧200

可以看到,即使外部状态改变了,内部状态依然是可以共享的。

享元模式角色

从上面两个示例中,我们可以把享元模式可以分为以下三种角色:

  • 抽象享元角色(Flyweight):享元对象的抽象基类或者接口,同时定义出对象外部状态和内部状态的实现接口。
  • 具体享元角色(ConcreateFlyweight):实现抽象类角色定义的业务。该角色内部状态处理应该与环境无关。
  • 享元工厂(FlyweightFactory):负责管理享元对象池和创建享元对象。享元模式一般都是和工厂模式一起出现。

享元模式在JDK源码中的体现

  • 1、String字符串
    Java中的String字符串当第一次使用之后,就会存在于常量池内,下次使用时可以直接从常量池内取出,不需要重复创建。具体关于String的特性,可以点击这里详细了解。
  • 2、Integer对象
    Integer大家都经常用,其实Integer中也使用到了享元模式。
    我们先看下面一个例子:
package com.zwx.design.pattern.flyweight;

public class TestInteger {
    
    
    public static void main(String[] args) {
    
    
        Integer a = Integer.valueOf(10);
        Integer b = 10;
        System.out.println(a==b);

        Integer m = Integer.valueOf(128);
        Integer n = 128;
        System.out.println(m==n);
    }
}

这里的输出结果可能会出乎很多人的意料之外:

true
false

第1句是true,第2句输出false。这好像有点神奇,我们来看看valueOf的源码:
在这里插入图片描述
这里就是用到了享元模式,首先会去缓存里面取,但是我们看到取缓存时候有个条件,那就是数字必须在low和high之间:
在这里插入图片描述
可以看到low是-128,high默认是127,所以我们取128的时候就不相等了。
当然,这里为什么限定-128~127之间才缓存,也只是个经验原因,在这个区间的数字是最常用的。另外还有Long中也用到了享元模式,在这里就不继续举例了。

PS:Integer b=10反编译之后其实执行的就是valueOf方法。

享元模式应用场景

当系统中多处需要用到一些公共信息时,可以把这些信息封装到一个对象实现享元模式,避免重复创建对象带来系统的开销。

享元模式主要用于系统中存在大量相似对象,且需要缓冲池的场景,一般情况下享元模式用于底层开发较多,以便提升系统性能。

享元模式优缺点

优点

减少对象的创建,降低了系统中对象的数量,故而可以降低系统的使用内存,提高效率。

缺点

  • 1、提高了系统的复杂度,需要注意分离出外部状态和内部状态。
  • 2、需要关注线程安全性问题

总结

本文主要介绍了享元模式的简单写法以及分离出内部状态和外部状态之后的写法,并结合JDK源码讲述了享元模式的应用场景。
请关注我,和孤狼一起学习进步

猜你喜欢

转载自blog.csdn.net/zwx900102/article/details/108554247
今日推荐