享元模式 尝试复用同类或相似的对象,以共享方式高效地支持大量的细粒度对象,减少面向对象的系统设计中可能创建大量类或对象,以减少内存占用和提高性能。
享元模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。
面向对象设计可以很好地解决一些灵活性或可扩展性问题,但在大多数情况下需要在系统中增加类和对象的个数。当对象数量过多时,会占用较大内存,甚至导致内存溢出,运行代价过高,带来性能下降等问题。
模式定义
享元模式(Flyweight Pattern):使用共享方式有效地支持大量的细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很少,可以实现对象的多次复用。
Flyweight
在拳击比赛中指最轻量级,即 羽量级,有的翻译为 蝇量级。共享模式要求能够共享的的对象必须是细粒度对象,所以又称为 轻量级模式。
特别需要理解的是,享元模式的重要意义是在结构上复用对象,而不在于创建对象。
模式分析
享元模式中的共享对象 被称为享元对象,享元对象能做到共享的关键是区分对象 内部状态(Internal State
)和 外部状态(External State
)。
一个内部状态是存储在享元对象内部的,并且是不会随环境改变而变化的。因此一个享元可以具有内部状态并可以共享。
一个外部状态是随环境变化而改变的,不可以共享的状态。享元对象的外部状态必交由客户端保存,并在享元对象被创建后,在需要使用时再传入享元对象内部。外部状态不可以影响享元对象的内部状态。也就是说,享元的内部状态与外部状态是相互独立的。
享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。其核心在于享元工厂提供一个用于存储享元对象的享元池,需要时从池中取出给客户端使用,不存在时创建对象存放池中。
模式优缺点
优点
- 相同对象只有一份,大大减少了系统中对象的数量,降低系统的内存,使效率提高。
缺点
- 为使对象可以共享,需要分离出外部状态和内部状态,需要将不能共享的状态外部化,这增加了系统的复杂度。
- 享元对象的外部状态需要客户端传入,这使得运行时间略有变长。
模式结构
享元模式包含以下角色:
- 抽象享元角色:是所有具体享元类的基类,定义具体享元规范需要实现的公共接口,非享元的外部状态以参数形式传入。
- 具体享元角色:实现抽象享元角色所规定的接口,如果有内部状态,必须为内部状态提供存储空间,内部状态的必须与环境无关。
- 非享元角色:不可共享的外部状态,包含了非共享的外部状态信息,以参数的形式注入具体享元的相关方法中。
- 享元工厂:负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个满足要求的享元对象;如果没有则创建。
- 客户端:需要维护一个对所有享元对象的引用,即要自行存储所有享元对象的外部状态。
结构图与代码
结构图
代码示例
抽象享元角色
public interface Flyweight {
void operation(UnsharedConcreteFlyweight state);
}
具体享元角色
@KeyAnnotation(key = "keyA")
public class ConcreteFlyweightA implements Flyweight {
// 内部状态
private static String innerState = "infoA";
@Override
public void operation(UnsharedConcreteFlyweight outState) {
System.out.println("ConcreteFlyweightA:" + outState.getInfo());
}
}
@KeyAnnotation(key = "keyB")
public class ConcreteFlyweightB implements Flyweight {
// 内部状态
private static String innerState = "infoB";
@Override
public void operation(UnsharedConcreteFlyweight outState) {
System.out.println("ConcreteFlyweightB:" + outState.getInfo());
}
}
/**
* @desc 注解
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface KeyAnnotation {
String key() default "";
}
享元工厂
创建和维护享元对象的工作,对象是能被系统共享的。
/**
* @desc 享元工厂
*/
public class FlyweightFactory {
private static HashMap<String, Flyweight> flyweightMap = new HashMap<String, Flyweight>();
public static Flyweight getFlyweight(String key) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Flyweight flyweight = flyweightMap.get(key);
if (!Objects.isNull(flyweight)) {
return flyweight;
} else {
Reflections reflections = new Reflections("com.designpatterns.structural.flyweight");
Set<Class<? extends Flyweight>> clazzs = reflections.getSubTypesOf(Flyweight.class);
Iterator<Class<? extends Flyweight>> iterator = clazzs.iterator();
while (iterator.hasNext()){
Class<? extends Flyweight> clazz = iterator.next();
KeyAnnotation annotation = clazz.getAnnotation(KeyAnnotation.class);
String key1 = annotation.key();
if (key.equals(key1)) {
Constructor<? extends Flyweight> constructor = clazz.getConstructor();
flyweight = constructor.newInstance();
flyweightMap.put(key, flyweight);
}
}
return flyweight;
}
}
}
非享元角色
/**
* @desc 非享元角色
*/
public class UnsharedConcreteFlyweight {
private String outState;
public UnsharedConcreteFlyweight(String outState) {
this.outState = outState;
}
public String getOutState() {
return outState;
}
public void setOutState(String outState) {
this.outState = outState;
}
}
客户端调用
public class MainTest {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Flyweight flyweight = FlyweightFactory.getFlyweight("keyA");
flyweight.operation(new UnsharedConcreteFlyweight("Hello"));
System.out.println();
}
}
// ConcreteFlyweightA:Hello
相关的模式
单例模式
两者较容易混淆,核心区别是能不能直接实例化。
享元模式:目的是共享,避免多次创建耗费资源,单纯的享元对象是可以直接实例化的。
单例模式:目的是限制创建多个对象,避免冲突,不可以直接实例化。比如使用数据库连接池。
工厂方法模式
享元工厂是一个特殊的工厂方法模式,特殊之处在于这个工厂维护一个所创建过的享元对象的记录,并根据这个记录和享元对象内部状态循环使用这些对象。
单例模式
享元工厂往往是单例模式。一个系统的享元,系统只需要一个享元工厂实例,所以可以设计为单例模式。
/**
* @desc 享元工厂
* @date 2021/11/2
*/
public class FlavorFactorySingleton {
private static FlavorFactorySingleton factory = new FlavorFactorySingleton();
private Map<String, Order> flavorMap = new HashMap<>();
private int totalFlavors = 0;
public static FlavorFactorySingleton getInstance(){
return factory;
}
public synchronized Order getOrder(String flavor) {
if (!flavorMap.containsKey(flavor)) {
flavorMap.put(flavor, new Flavor(flavor));
}
return flavorMap.get(flavor);
}
public int getTotalFlavors() {
return flavorMap.size();
}
}
不变模式
享元模式里的享元对象不一定非得是不变对象(Immutable),但是很多的享元对象确实被设计成了不变对象。
由于不变对象的状态在被创建之后就不再变化,因此不变对象满足享元模式对享元对象的要求。
备忘录模式
享元工厂负责维护一个表,通过把这个表很多全同的实例与代表他们的一个对象联系起来。这就是备忘录模式的应用。
合成模式
严格地讲,享元模式并不是一个单纯的模式,而是一个由数个模式组合而成的复合模式。
如,享元模式中的工厂角色是一个工厂方法模式,只是内部记录了所创建的实例,并选择使用这些实例。
享元模式是合成模式的应用。抽象享元角色是复合构件角色,而具本享元角色应是具体构件角色。复合享元是树枝构件,而单纯享元是树叶构件。
适用场景
- 系统有大量相似对象,这些对象消耗大量内存。
- 这些对象的状态大部分可外部化,共性部分可复用。
- 这些对象可以按内部状态分成很多组,当把外部状态抽离时,每一组可以仅用一个对象代替。
- 软件系统依赖的是对象,而不依赖这些对象的身份。
另,使用享元模式需要耗费资源来维护一个享元表,应当在有足够多的享元实例需要共享时才值的使用享元模式。——都2021了,资源问题几乎不存在。
应用示例
示例:售卖不同口味的咖啡给坐在不同桌号的客户。
分析:咖啡本身的各种口味是内部属性是不会变的,所以各种口味咖啡可以抽取作为享元对象来使用;不同桌号是外部状态,作为参数传入。
抽象享元角色
/**
* @desc 抽象享元角色
* @date 2021/11/2
*/
public abstract class Order {
/**
* 把咖啡卖给客人
* @param table
*/
public abstract void serve(Table table);
/**
* 特点,名称
* @return
*/
public abstract String getFlavor();
}
具体享元角色
/**
* @desc 具体享元
* @date 2021/11/2
*/
public class Flavor extends Order {
private String flavor;
public Flavor(String flavor) {
this.flavor = flavor;
}
@Override
public void serve(Table table) {
System.out.println("Serving table:" + table.getNumber() + ", flavor:" + flavor);
}
@Override
public String getFlavor() {
return this.flavor;
}
}
享元工厂
/**
* @desc 享元工厂
* @date 2021/11/2
*/
public class FlavorFactory {
private Map<String, Order> flavorMap = new HashMap<>();
private int totalFlavors = 0;
public Order getOrder(String flavor) {
if (!flavorMap.containsKey(flavor)) {
flavorMap.put(flavor, new Flavor(flavor));
}
return flavorMap.get(flavor);
}
public int getTotalFlavors() {
return flavorMap.size();
}
}
非享元角色
public class Table {
private int number;
public Table() {
}
public Table(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
客户端
public class ClientTest {
private static FlavorFactory factory;
public static void main(String[] args) {
factory = new FlavorFactory();
Order blackCoffee = factory.getOrder("Black Coffee");
blackCoffee.serve(new Table(100));
Order cappuccino = factory.getOrder("Cappuccino");
cappuccino.serve(new Table(85));
System.out.println("Coffee Types:" + factory.getTotalFlavors());
}
}
重构应用
享元模式并不是一个非常常见的模式,但在某些情况下,享元模式可能成为重构的强大武器。
重构系统时,可能发现系统存在非常多的常规类的实例,而所有这些实例的状态只有非常少的几种。
那这块就可以作为重构的一个点,系统其实并不需要这么多的独立实例,则只需要为每一种不同的状态创建一个实例,让整个系统共享这些很少的实例。即使用 享元模式 来重构系统。
一个享元对象只含有可以共享的状态,而没有不可共享的状态,这是使用享元模式的前提。
重构分两步走:
将可以共享的状态和不可以共享的状态从常规类中分离,抽离不可共享的状态。
这个类的创建过程必须由一个工厂对象加以控制。
为达到共享目的,客户端不可以直接创建被共享对象,而应当使用一个工厂对象负责创建被共享对象。
这个工厂对象内部有一个列表用于保存所有已经创建出来的享元对象。当有客户端请求一个新对象时,工厂先检查列表是否存在,存在则取出返回;没有则创建一个新对象。
典型应用
常量池中使用
Java 中 String 类由 final 修饰,即不可改变的。在 JVM 中,字符串一般被保存在字符串常量池中,且 Java 会确保一个字符串在常量池中只有一份。
常量池(Constant Pool)指的是在编译期被确定,并被保存在已编译的 .class 文件中的一些数据。它包括了关于类、方法、接口、字符串等常量。字符串常量池指对应常量池中存储 String 常量的区域。
下面我们做一个简单的测试,代码如下:
public class StringTest {
public static void main(String[] args) {
String s1 = "HelloWorld";
String s2 = "HelloWorld";
String s3 = "Hello" + "World";
String s4 = "Hello" + new String("World");
String s5 = new String("HelloWorld");
String s6 = s5.intern();
String s7 = "Hello";
String s8 = "World";
String s9 = s7 + s8;
System.out.println(s1 == s2); //true
System.out.println(s1 == s3); //true, s3会被优化处理为常量
System.out.println(s1 == s4); //false, s4会在堆中申请新的空间生成新的引用
System.out.println(s1 == s9); //false
System.out.println(s4 == s5); //false
System.out.println(s1 == s6); //true, 将字面量做常量处理
}
}
对于以字面量形式创建的 String 变量,JVM 会在编译期间就把该字面量的值 hello
放到字符串常量池中,这样 Java 启动的时候就已经加载到内存中了。而用 new String()
创建的字符串不是常量,不能在编译期就确定,所以 new String()
创建的字符串不放入常量池中,它们有自己的地址空间。
字符串常量池的特点就是有且只有一个相同的字面量。如果有其他相同的字面量,则 JVM 返回这个字面量的引用;如果没有相同的字面量,则在字符串常量池中创建这个字面量并返回它的引用。
存在于 .class 文件中的常量池在运行期被 JVM 装载,并且可以扩充。而 String 的 intern() 方法就是扩充常量池的一个方法。intern() 方法能使一个位于堆中的字符串在运行期间动态地加入字符串常量池(字符串常量池的内容是在程序启动的时候就已经加载好了的)。
调用 intern() 方法时,Java 会查找字符串常量池中是否有该对象对应的字面量,如果有,则返回该字面量在字符串常量池中的引用;如果没有,则复制一份该字面量到字符串常量池并返回它的引用,因此 s1==s6 输出 true。
实现数据库连接池
将数据库连接作为对象存储在一个 Vector 对象中。将 Connection 对象在调用前创建好并缓存起来,在用的时候直接从缓存中取值,用完后再放回去,复用这些已经建立的库连接,大大节省系统资源和时间。
public class ConnectionPool {
private static ArrayList<Connection> pools;
private static String url = "jdbc:mysql://localhost:3306/db_name";
private static String username = "root";
private static String password = "123";
private static String driverClassName = "com.mysql.jdbc.Driver";
private static int poolSize = 10;
// 缓存
static {
cacheConnection();
}
public static void cacheConnection() {
pools = new ArrayList<>(poolSize);
try {
Class.forName(driverClassName);
for (int i = 0; i < poolSize; i++) {
Connection conn = DriverManager.getConnection(url, username, password);
pools.add(conn);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized Connection getConnection() {
if (CollectionUtils.isEmpty(pools)) {
cacheConnection();
}
Connection connection = pools.get(0);
pools.remove(connection);
return connection;
}
public synchronized void release(Connection conn) {
pools.add(conn);
}
}
相关参考
注意:本文归作者所有,未经作者允许,不得转载