I walk very slowly, but I never walk backwards
设计模式 - 享元模式
寂然
大家好,我是寂然,本节课,我们来聊设计模式中的享元模式,老规矩,首先我们先通过一个案例需求来引入
案例演示 - 网站项目
首先我们来看这样一个需求:
我们接了一个小型的外包项目,给客户老王做一个产品展示网站,老王的朋友们感觉效果不错,也希望做这样的产品展示网站,但是他们要求都有些不同
1)有客户要求以新闻的形式发布
2)有客户人要求以博客的形式发布
3)有客户希望以微信公众号的形式发布
解决方案一:一般实现
OK,拿到这样一个需求以后,我们先不要考虑今天要聊的享元模式,首要目标是解决需求,因为需求中虽然网站的发布形式不一样 ,但是基本的形式是一致的,所以最容易想到的方式是直接复制粘贴一份,然后根据客户不同要求,进行定制修改,接着,放到不同的虚拟空间中,如下示意图
方案分析
其实上面实现方式是我们经常用的,复制一份进行定制修改,就要求客户需要的网站结构相似度很高,只是展示的形式和部分微观结构需要定制,而且这些都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,会造成服务器的资源浪费
那其实我们可以考虑这样做,整合到一个网站中,共享其相关的代码和数据,对于硬盘、内存、CPU、数据库空间等服务器资源都尝试达成一个共享的效果,这样不仅减少了需要的服务器资源,而且对于代码来说,由于是一份实例,维护和扩展都更加容易,这种解决思路其实就是享元模式
基本介绍
享元模式(Flyweight Pattern)也叫蝇量模式:运用共享技术有效地支持大量细粒度的对象
属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式,享即共享,元即对象
常用于系统底层开发,解决系统的性能问题,像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则重新创建
再一个,享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似的对象,需要缓冲池时,不需要总是创建新对象,可以从缓存池里拿,这样可以降低系统内存,同时提高效率,所以,享元模式最经典的应用场景就是池技术了,String 常量池,数据库连接池,缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式
原理类图
享元模式的类图如下图所示
享元模式角色分析
-
Flyweight 是抽象的享元角色,它是产品的抽象类,同时它会定义出对象的外部状态和内部状态
-
ConcreteFlyweight 是具体的享元角色,是具体的产品类,实现抽象角色定义相关业务
-
UnsharedConcreteFlyweight 是不可共享的角色,这个角色可能会出现在享元模式中,但是一般不会出现在享元工厂中
-
FlyweightFactory 是享元工厂类,用于构建一个池的容器(以集合的形式展现),同时提供从池中获取对象的相关方法
外部状态&内部状态
享元模式提出了两个要求,细粒度和共享对象,这里涉及到了两个概念,内部状态与外部状态,即将对象的信息分为两个部分,内部状态与外部状态
内部状态指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变
外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态
比如围棋、五子棋、跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,所以棋子颜 色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同,当我们落子后,落子颜色是定的,但位置是变化 的,所以棋子坐标就是棋子的外部状态
为什么要这样做
围棋理论上有 361 个空位可以放棋子,每盘棋都有可能有两三百个棋子对象产生,因为内存空间有限,一台服务器很难支持更多的玩家玩围棋游戏,如果用享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例,这样就减少了创建对象的数量,降低了重复对象的创建,减少内存占用和提高性能
解决方案二:享元模式
下面我们使用享元模式重构案例需求,首先我们通过类图来演示解决的思路
原理类图
代码演示
//抽象的享元角色
public abstract class Website {
public abstract void show(User user);//抽象方法
}
//具体的享元角色
public class ConcreteWebsite extends Website {
//共享的部分,不会变属于内部状态
private String type = "";//网站发布的形式
//构造器
public ConcreteWebsite(String type) {
this.type = type;
}
//外部状态??例如用户
@Override
public void show(User user) {
System.out.println("以" + type + "形式发布网站项目,用户为:" + user.getName());
}
}
//享元工厂类
public class WebsiteFactory {
//创建集合,充当池的角色
private HashMap<String,ConcreteWebsite> pool = new HashMap<>();
//根据网站的类型,返回对应的网站,如果没有就创建网站并放入池中
public Website getWebsite(String type){
if (!pool.containsKey(type)){
//如果没有对应的类型,就创建一个放入
pool.put(type,new ConcreteWebsite(type));
}
return (Website)pool.get(type);
}
//获取网站分类的总数
public int getWebsiteCount(){
return pool.size();
}
}
//用户
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//客户端
public class Client {
public static void main(String[] args) {
//创建一个工厂
WebsiteFactory websiteFactory = new WebsiteFactory();
//客户要一个以新闻形式发布的网站
Website news = websiteFactory.getWebsite("news");
news.show(new User("老李"));
//客户需要以微信公众号形式发布的网站
Website wechat = websiteFactory.getWebsite("Wechat");
wechat.show(new User("老王"));
//多个客户需要微信公众号形式发布
Website wechat1 = websiteFactory.getWebsite("Wechat");
wechat1.show(new User("老赵"));
Website wechat2 = websiteFactory.getWebsite("Wechat");
wechat2.show(new User("老沈"));
//看看池子里的总数
System.out.println(websiteFactory.getWebsiteCount());
}
}
注意事项
优势
-
节省内存空间,重复对象需要频繁被创建时,由于只会被创建一次,所以对系统内存的需求也大大减小
-
提高效率,对于可重复的对象只会被创建一次,再次访问时先从缓冲池中获取,响应速度更快,效率更高
注意点
享元模式提高了系统的复杂度,需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方,使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制,如果盲目使用, 会提高系统逻辑的复杂度
使用场景
-
享元模式经典的应用场景就是各类池技术
-
系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用 HashMap/HashTable等来进行存储
Integer源码分析
OK,其实我们最常用的 java.lang.Integer 中,就使用到了享元模式,这节课我们一起来看下 Integer 源码中,享元模式的使用,在此之前呢,我们一起来聊几道 Integer 相关的面试,作为前置基础来引出我们要聊的内容
Int&Integer面试题解析
首先我们来看如下几个案例,对结果进行判定
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false
由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)
Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true
Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true
Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false
对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
java在编译Integer i = 100 ;
时,会翻译成为Integer i = Integer.valueof(100);
通过源码我们得知,默认对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,不会再次进行new操作
测试代码
OK,明确了上面的几点后,我们来看这样一段测试代码
public class Test {
public static void main(String[] args) {
Integer a = Integer.valueof(127);
Integer b = new Integer(127);
Integer c = Integer.valueof(127);
System.out.println(a.equals(b)); //比较大小,为true
System.out.println(a == b); //false
System.out.println(a == c); //true
}
}
有了上面的前置基础,判断就很容易了,同时上面我们聊到,说对于传入的127会进行缓存,a是通过valueof方法传入127返回的对象实例,c也是如此,所以为true,下面我们通过源码来验证一下这个结论,而且同时大家也会想到,这其中就使用到了享元模式
Integer源码解析
我们进入 Integer 的 valueof 方法,如果在定义好的范围以内,会直接从下面的缓存池里去拿,如果不再该范围以内,return new Integer(i);
public static Integer valueof(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
默认定义好的范围是多少呢?我们接着往下看,默认缓存的范围是-128到127之间的数
也就是说,如果 Integer.valueOf(x) ,x满足这个范围,就是使用享元模式返回,而享元模式中,如果第一次没有,进行创建,有就直接返回,如果不在该范围,仍然创建一个新的对象
接着看,它在第一次创建 IntegerCache 的时候,首先创建了 cache 数组,大小为 (high - low) + 1,是因为数据下标不可以为负数,也就是创建了可以存放 -128 - 127 之间的数组,然后往里面一个个添加数据
valueOf 方法,就使用到享元模式,如果使用 valueOf 方法得到一个 Integer 实例,范围在 -128 - 127,直接返回已经创建好的,执行速度是要大于 new 操作的,当然,不光是 Integer 内部维护的缓冲池,String 常量池,数据库连接池,等等都是享元模式的应用,享元模式是池技术的重要实现方式
下节预告
OK,到这里,享元模式的相关内容就结束了,下一节,我们开启代理模式的学习,最后,希望大家在学习的过程中,能够感觉到设计模式的有趣之处,高效而愉快的学习,那我们下期见~