大家好,我是老田,今天我给大家分享设计模式中的享元模式。用贴切的生活故事,以及真实项目场景来讲设计模式,最后用一句话来总结这个设计模式。
下面是本文目录:
背景
享元模式(Flyweight Pattern)又叫作轻量级模式,是对象池的一种实现。
类似线程池,线程池可以避免不停地创建和销毁多个对象,消耗性能。
享元模式提供了减少对象数量从而改善应用所需的对象结构的方式。
英文解释:
Use sharing to support large numbers of fine-grained objects efficiently.
享元模式(Flyweight Pattern)其宗旨是共享细粒度对象,将多个对同一对象的访问集中起来,不必为每个访问者都创建一个单独的对象, 主要用于减少创建对象的数量,以减少内存占用和提高性能。
属于结构性设计模式,其中结构性设计模式有:代理、门面、装饰器、享元、桥接、适配器、组合。
注意:
享元模式把一个对象的状态分成内部状态和外部状态,内部状态是不变的,外部状态是变化的;然后通过共享不变的部分,达到减少对象数量并节约内存的目的。
生活案例
房屋中介
只要是个城市,就少不了房屋中介,房屋中介存有大量的出租房屋信息,并且一家房屋中介往往会有多个门店,但是所有门店都共享这些房屋信息(共享的是出租房屋的信息)。
个人身份证信息
每个中国公民都有一张身份证,并且这张身份证信息在公安系统中是共享的,全国各公安局派出所都会共享你的身份证信息(共享的是个人身份信息)。
高考志愿填报
每所大学在每个省都有明确的招收名额,这些名额对于该省的所有高考生而言都是共享的(共享的是招收名额)。
图书馆
图书馆里的可借书籍,对多有读者是共享的,大家都可以查询此书是否已经被借出去,还剩基本可借(共享的是图书)。
....
简单代码实现
下面我们通过一个案例来演示享元模式(图书馆为例)。
- public interface Book {
- void borrow();
- }
- /**
- * @author java后端技术全栈
- */
- public class ConcreteBook implements Book {
- //被借出去的书名
- private String name;
- public ConcreteBook(String name) {
- this.name = name;
- }
- @Override
- public void borrow() {
- System.out.println("图书馆借出去一本书,书名:"+this.name);
- }
- }
- import java.util.HashMap;
- import java.util.Map;
- /** 图书馆
- * @author java后端技术全栈
- */
- public class Llibrary {
- private Map<String, Book> bookMap = new HashMap<>();
- private Llibrary() {
- }
- //只能有一个图书馆
- public static Llibrary getInstance() {
- return LazyHolder.LAZY_STATIC_SINGLETON;
- }
- //通过书名name来借书
- public Book libToBorrow(String name) {
- Book book;
- //如果图书馆有,直接把书借走
- if (bookMap.containsKey(name)) {
- book = bookMap.get(name);
- } else {//图书馆没有,则录入一本书,然后把书借走
- book = new ConcreteBook(name);
- bookMap.put(name, book);
- }
- return book;
- }
- //返回还有多少本书
- public int bookSize() {
- return bookMap.size();
- }
- private static class LazyHolder {
- private static final Llibrary LAZY_STATIC_SINGLETON = new Llibrary();
- }
- }
- import java.util.ArrayList;
- import java.util.List;
- public class Student {
- private static List<Book> bookList = new ArrayList<>();
- private static BookFactory bookFactory;
- public static void main(String[] args) {
- bookFactory = BookFactory.getInstance();
- studenBorrow("java 从入门到精通");
- studenBorrow("java 从入门到放弃");
- studenBorrow("JVM java虚拟机");
- studenBorrow("java编程思想");
- //还了后,再借一次
- studenBorrow("java 从入门到精通");
- studenBorrow("java 从入门到放弃");
- studenBorrow("JVM java虚拟机");
- studenBorrow("java编程思想");
- //还了后,再借一次
- studenBorrow("java 从入门到精通");
- studenBorrow("java 从入门到放弃");
- studenBorrow("JVM java虚拟机");
- studenBorrow("java编程思想");
- //把每一本书借出去
- for (Book book:bookList){
- book.borrow();
- }
- System.out.println("学生一共借了 "+bookList.size()+"本书");
- System.out.println("学生一共借了 "+ bookFactory.bookSize()+"本书");
- }
- private static void studenBorrow(String name) {
- bookList.add(bookFactory.libToBorrow(name));
- }
- }
运行结果
- 图书馆借出去一本书,书名:java 从入门到精通
- 图书馆借出去一本书,书名:java 从入门到放弃
- 图书馆借出去一本书,书名:JVM java虚拟机
- 图书馆借出去一本书,书名:java编程思想
- 图书馆借出去一本书,书名:java 从入门到精通
- 图书馆借出去一本书,书名:java 从入门到放弃
- 图书馆借出去一本书,书名:JVM java虚拟机
- 图书馆借出去一本书,书名:java编程思想
- 图书馆借出去一本书,书名:java 从入门到精通
- 图书馆借出去一本书,书名:java 从入门到放弃
- 图书馆借出去一本书,书名:JVM java虚拟机
- 图书馆借出去一本书,书名:java编程思想
- 学生一共借了 12本书
- 学生一共借了 4本书
其实,图书馆只有四本书,但是多个人借,A借来看完了,B再去借,B还了C再去借。
这些书籍就被大家共享了。
享元模式的UML类图如下:
由上图可以看到,享元模式主要包含3个角色。
- 抽象享元角色(Book):享元对象抽象基类或者接口,同时定义出对象的外部状态和内部状态的接口或实现。
- 具体享元角色(ConcreteBook):实现抽象角色定义的业务。该角色的内部状态处理应该与环境无关,不会出现一个操作改变内部状态、同时修改了外部状态的情况。
- 享元工厂(BookFactory):负责管理享元对象池和创建享元对象。
也许这个例子你还是不太明白,下面我们就用工作中常见的场景来解释一通。
大佬们是怎样使用的
关于享元模式,在JDK中大量的使用,比如:String、Integer、Long等类中,都有使用到。
Integer中的享元模式
下面这段代码输出什么?
- /**
- * 欢迎关注公众号:java后端技术全栈
- *
- * @author 田维常
- * @date 2021/06/02 19:30
- */
- public class IntegerDemo {
- public static void main(String[] args) {
- Integer a = 100;
- Integer b = Integer.valueOf(100);
- System.out.println(a == b);
- Integer c = new Integer(1000);
- Integer d = Integer.valueOf(1000);
- System.out.println(c == d);
- }
- }
很多人可能会认为输出
- true
- true
其实,非也,这里最终输出的是:
- true
- false
为什么呢?100就可以比较,1000就不能比较了?
其实,在Integer里就用到了享元模式,它就是把-128到127这个范围的数据缓存起来(放在Integer类型的数组中)。
- static final int low = -128;
- public static Integer valueOf(int i) {
- //high默认是127
- if (i >= IntegerCache.low && i <= IntegerCache.high)
- return IntegerCache.cache[i + (-IntegerCache.low)];
- return new Integer(i);
- }
下面进行一个简要的分析:
关于Integer的缓存,推荐看这篇文章:
这里Integer里的IntegerCache里就用到了享元模式。
关于Integer 推荐:面试官:说说Integer缓存范围
String中的享元模式
Java中讲String类定义为final不能继承,并且将属性value也定义为final便是不可变,JVM中字符串一般保存在字符串常量池中,Java会确保一个字符串在常量池中只会有一份拷贝,这个字符串常量池在JDK1.6中位于方法区(永久代)中,而JDK1.7以后,JVM讲其从方法区移动到了堆heap中。
下面这段代码输出什么?
- /**
- * 欢迎关注公众号:java后端技术全栈
- *
- * @author 田维常
- * @date 2021/06/03
- */
- public class StringDemo {
- public static void main(String[] args) throws Exception {
- String s1 = "abcd";
- String s2 = "abcd";
- String s3 = "ab" + "cd";
- String s4 = "ab" + new String("cd");
- String s5 = new String("abcd");
- String s6 = s5.intern();
- String s7 = "a";
- String s8 = "bcd";
- String s9 = s7 + s8;
- System.out.println("s1 == s2 " + (s1 == s2));
- System.out.println("s1 == s3 " + (s1 == s3));
- System.out.println("s1 == s4 " + (s1 == s4));
- System.out.println("s1 == s6 " + (s1 == s6));
- System.out.println("s1 == s9 " + (s1 == s9));
- System.out.println("s4 == s5 " + (s4 == s5));
- }
- }
String类中的value是final修饰的,以字面量的形式创建String变量时,JVM会在编译期间就把该字面量“abcd”放到字符串常量池汇总,有Java程序启动的时候就已经加载到内存中了。这个字符串常量的特点就是有且仅有一份相同的字面量,如果其他相同字面量,JVM则返回这个字面量的引用,如果没有相同的字面量,则再字符串常量池中创建这个字面量并返回它的引用。
由于s2指向字面量"abcd"在常量池中已经存在了(s1先于s2),于是JVM就返回这个字面量绑定的引用,所以s1==s2。
s3中字面量的拼接其实在JVM层已经做了优化,在JVM编译期间就对s3的拼接做了优化,所以s1、s2、s3都可以理解为是同一个,即s1==s3。
s4中的new String("cd"),此时生成了两个对象,"cd"和new String("cd"),"cd"存在于字符串常量池中,new String("cd")存在于堆heap中,String s4="ab"+ new String("cd");实质上是两个对象的相加,编译器不会对其进行优化,相加的结果存在于堆heap中,而s2存在于字符串常量池中,当然不相等,即s1!=s4。
s4和s5最终的结果都是在堆中,所以此时s4!=s5
s5.intern()方法能是一个维度对总的字符串在运行期间动态地加入到字符串常量池中(字符串常量池的内容是程序启动的时候就以及酒精加载好了,如果字符串常量池中存在该对象对应的字面量,则返回该字面量在字符串常量池中的引用,否则,创建复制一份该字面量到字符串常量池中并发那会它的引用),因此s1==s6。
s9是s7和s8拼接而成,但是jvm并没有对其进行优化,所以s1!=s9
最后,上面这段代码输出:
- s1 == s2 true
- s1 == s3 true
- s1 == s4 false
- s1 == s6 true
- s1 == s9 false
- s4 == s5 false
JVM中的常量池也是享元模式的经典实现之一。
关于String延伸内容:
美团面试题:String s = new String("111")会创建几个对象?
Long中的享元模式
Long中和Integer中类似,也是最-128到127的数进行了缓存,请看Long中的valueOf()方法源码部分:
- public static Long valueOf(long l) {
- final int offset = 128;
- if (l >= -128 && l <= 127) { // will cache
- return LongCache.cache[(int)l + offset];
- }
- return new Long(l);
- }
这个就没必要进行演示了,和Integer一样,都是使用了缓存,也就是享元模式。
在Apache Commons Pool中的享元模式
对象池化的基本思路是:将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象造成的消耗。用于充当保存对象的“容器”的对象,被称为对象池(Object Pool,简称Pool)。
Apache Pool实现了对象池的功能,定义了对象的生成、销毁、激活、钝化等操作及其状态转换,并提供几个默认的对象池实现,
有如下几个重要的角色:
- Pooled Object(池化对象):用于封装对象(例如,线程、数据库连接和TCP连接),将其包裹成可被对象池管理的对象。
- Pooled Object Factory(池化对象工厂):定义了操作Pooled Object实例生命周期的一些方法,Pooled Object Factory必须实现线程安全。
- Object Pool(对象池):Object Pool负责管理Pooled Object,例如,借出对象、返回对象、校验对象、有多少激活对象和有多少空闲对象。
在ObjectPool类的子类org.apache.commons.pool2.impl.GenericObjectPool种有个属性:
- private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects;
这个Map就是用来缓存对象的,所以这里也是享元模式的实现。
享元模式的扩展
享元模式中的状态
享元模式的定义提出了两个要求:细粒度和共享对象。
因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。
内部状态指对象共享出来的信息,存储在享元对象内部,并且不会随环境的改变而改变;
外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。
比如:连接池中的连接对象,保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。
优缺点
优点
- 减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率。
- 减少内存之外的其他资源占用。
缺点
- 关注内、外部状态,关注线程安全问题。
- 使系统、程序的逻辑复杂化。
总结
享元模式,单从概念来讲估计很多人不是很理解,但是从Integer、String已经生活中的场景结合起来理解,就能轻松理解享元模式,享元模式的实现基本上都伴随着一个集合用来存这些对象。
一句话总结:
优化资源配置,减少资源浪费
参考:Tom的设计模式课程
好了,今天的分享就到此结束,希望大家能明白什么是享元模式,享元模式的思想我们在开发中是否能借鉴,面试的时候就不要再说你不会设计模式了。
本文转载自微信公众号「Java后端技术全栈」,可以通过以下二维码关注。转载本文请联系Java后端技术全栈公众号。