Java设计模式学习记录-享元模式
前言
享元模式也是一种结构型模式,这篇是介绍结构型模式的最后一篇了(因为代理模式很早之前就已经写过了)。享元模式采用一个共享来避免大量拥有相同内容对象的开销。这种开销最常见、最直观的就是内存损耗。
享元模式
定义
享元模式是指运用共享技术有效的支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
举例
咖啡问题,在一家咖啡店里有几种口味的咖啡,例如:拿铁、摩卡、卡布奇诺等等。最近这家店在搞促销活动,一中午能卖出几百杯咖啡,那么咖啡的口味就是一种共享的元素。下面用代码来实现一下这个例子。
定义订单接口
/** * 订单接口 */ public interface Order { //卖出咖啡 void sell(); }
具体的订单实现
public class CoffeeOrder implements Order { //咖啡口味 public String flavor; public CoffeeOrder(String flavor){ this.flavor = flavor; } @Override public void sell() { System.out.println("卖出了一份"+flavor+"的咖啡。"); } }
订单工厂类
import com.google.common.collect.Maps; import java.util.Map; import java.util.Objects; /** * 订单工厂类 */ public class CoffeeOrderFactory { private static Map<String,Order> cof = Maps.newHashMap(); /** * 获得订单 * @param flavor 口味 * @return */ public static Order getOrder(String flavor){ Order order = cof.get(flavor); if(Objects.isNull(order)){ order = new CoffeeOrder(flavor); cof.put(flavor,order); } return order; } /** * 获取最终创建的对象个数 * @return */ public static int getSize(){ return cof.size(); } }
测试类
import org.assertj.core.util.Lists; import java.util.List; /** * 测试买咖啡 */ public class MyTest { public static void main(String[] args) { buyCoffee("拿铁"); buyCoffee("卡布奇诺"); buyCoffee("摩卡"); buyCoffee("拿铁"); buyCoffee("拿铁"); buyCoffee("拿铁"); buyCoffee("卡布奇诺"); buyCoffee("卡布奇诺"); buyCoffee("卡布奇诺"); buyCoffee("摩卡"); buyCoffee("摩卡"); buyCoffee("摩卡"); //打印出卖出的咖啡 coffeeOrderList.stream().forEach(Order::sell); System.out.println("一共卖出去"+coffeeOrderList.size()+"杯咖啡!"); System.out.println("一共生成了"+CoffeeOrderFactory.getSize()+"个Java对象!"); } //订单列表 public static List<Order> coffeeOrderList = Lists.newArrayList(); /** * 买咖啡 * @param flavor 口味 */ public static void buyCoffee(String flavor){ coffeeOrderList.add(CoffeeOrderFactory.getOrder(flavor)); } }
运行结果
卖出了一份拿铁的咖啡。
卖出了一份卡布奇诺的咖啡。
卖出了一份摩卡的咖啡。
卖出了一份拿铁的咖啡。
卖出了一份拿铁的咖啡。
卖出了一份拿铁的咖啡。
卖出了一份卡布奇诺的咖啡。
卖出了一份卡布奇诺的咖啡。
卖出了一份卡布奇诺的咖啡。
卖出了一份摩卡的咖啡。
卖出了一份摩卡的咖啡。
卖出了一份摩卡的咖啡。
一共卖出去12杯咖啡!
一共生成了3个Java对象!
从上面的运行结果可以看出来,虽然卖出去了12杯咖啡,但是最终的口味对象只有3个,因为咖啡口味只有在第一次使用的时候创建,后面就直接使用不会再创建了。
享元模式的分析
下面还是来分析一下享元模式的结构吧,结构图如下:
享元模式涉及到的角色有抽象享元角色、具体享元角色、复合享元角色、享元工厂角色,以及客户端角色。具体说明如下:
- 抽象享元角色(FlyWeight):此角色是所有具体享元类的父级,为这些类规定出需要实现的公共接口或抽象类。上面例子中的Order接口就是代表的这个角色。
- 具体享元角色(ConcreteFlyweight):实现抽象享元角色所规定的接口。有时候具体享元角色有称为单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的。上面例子中的CoffeeOrder就是代表的这个角色。
- 复合享元角色(UnsharableFlyweight):复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合对象又称为不可共享对象。这个角色一般很少用。
- 享元工厂角色(FlyweightFactory):负责创建和管理享元角色。此角色必须保证享元对象可以被系统适当地共享。当客户端对象请求一个享元对象时,享元工厂角色需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象。上面例子中的CoffeeOrderFactory就是代表的这个角色。
- 客户端角色(client):此角色调用享元工厂角色来使用具体享元角色。
享元模式总结
单纯享元模式和复合享元模式
标准的享元模式中既包含享元对象又包含非享元对象,但是在实际使用过程中我们会用到具体两种特殊形式的享元模式:单纯享元模式和复合享元模式。
单纯享元模式是指,所有的具体享元对象都是可以共享的,不包括非享元对象。
复合享元模式是指,将一些单纯享元对象使用组合模式加以组合,还可以形成组合享元对象,这样的复合享元对象不能共享,但是它可以分解成单纯享元对象,分解后就可以共享了。
享元模式的优点
1、可以极大的减少内存中对象的数量,使得相同或相似的对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。
2、享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
享元模式的缺点
1、享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
想了解更多的设计模式请查看Java设计模式学习记录-GoF设计模式概述。
面试面到怀疑人生,继续加油吧!
2、为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。
适用场景
一个系统有大量相同或者相似的对象,造成内存的大量耗费。
对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
延伸
在JDK中就有使用享元模式的例子,最常见的就是我们使用的String类,大家都知道String类是被final修饰的,所以不会被继承,每次变更都会生成一个新的字符串,这样就有点占内存了。所以如果直接写出了一个字符串,当后面又写出了一个同样的字符串时会自动去堆中(JDK7以上)查看是否已经存在这个字符串了,如果已经存在则直接使用,如果不存在这个时候才在堆中再给开辟一块空间存储字符串。
例如下面的例子:
运行结果: