Java - GoF设计模式详解11(享元模式)
作者:hangge | 2023-04-30 08:43
十一、享元模式
1,基本介绍
(1)享元模式(Flyweight)又叫做蝇量模式,指运用共享技术实现大量细粒度对象的复用,从而节省创建对象所需要分配的空间,以减少内存占用和提高性能。(享元即指被共享的单元)。
(2)该模式中包含的角色及其职责如下:
- 享元接口(Flyweight):通常是一个抽象类或者接口,定义了对象的外部状态和内部状态的接口或者实现。
- 具体享元类(Concrete Flyweight):实现了享元接口,完成具体业务。
- 享元工厂(Flyweight Factory):负责管理享元对象池和创建享元对象。
2,使用样例
(1)这里以俄罗斯方块为例演示享元模式的使用。游戏时每次会随性出现一种形状的方块下落,如果每次都要创建一个新的方块对象,那么一局下来就要创建大量的对象,十分浪费资源。使用享元模式则可以解决这个问题,每次需要显示一个新方块时,先尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。
(2)首先我们定义一个享元接口,定义了方块的共有方法:
// 方块接口(享元接口) public interface Block { //显示形状及颜色 void display(String color); }
(2)接着定义一个具体的方块类来实现这个接口:
//具体方块(具体享元类) public class ConcreteBlock implements Block { //方块形状 private String shape; ConcreteBlock(String shape) { this.shape = shape; } @Override public void display(String color) { System.out.println("显示"+ color + "色的" + this.shape +"形方块"); } }
(3)然后创建一个享元工厂来维护我们所有的享元对象,这里我们使用 HashMap 作为池容器来存储具体的享元对象,并且在需要的时候可以从池中获取已经创建的享元对象。
// 方块工厂(享元工厂) public class BlockFactory { // 方块池 private HashMap<String, Block> blocks = new HashMap<>(); // 获取指定形状的方块 public Block getBlock(String shape) { Block block = blocks.get(shape); if (block == null) { block = new ConcreteBlock(shape); blocks.put(shape, block); } return block; } }
(4)最后测试一下,可以看到对于相同形状的方块使用的是同一个对象:
public class Test { @SneakyThrows public static void main(String[] args) { BlockFactory blockFactory = new BlockFactory(); Block block1 = blockFactory.getBlock("Z"); block1.display("红"); Block block2 = blockFactory.getBlock("Z"); block1.display("蓝"); Block block3 = blockFactory.getBlock("T"); block3.display("蓝"); Block block4 = blockFactory.getBlock("L"); block4.display("绿"); Block block5 = blockFactory.getBlock("T"); block5.display("绿"); System.out.println("block1与block2是否为同一对象:" + (block1==block2)); System.out.println("block2与block3是否为同一对象:" + (block2==block3)); System.out.println("block3与block5是否为同一对象:" + (block3==block5)); } }
附一:JDK 中的享元模式
(1)在 Java 中的 java.lang.Integer 类就使用了享元模式。Integer 类提供了一个静态方法 valueOf(),它可以将一个整数转换为 Integer 对象。这个方法使用了享元模式,因为它只会在内存中保存 -128 到 127 之间的整数对应的 Integer 对象。对于超出这个范围的整数,每次调用 valueOf() 都会创建一个新的 Integer 对象。
注意:实际上,除了 Integer 类型之外,其他包装器类型,比如 Long、Short、Byte 等,也都利用了享元模式来缓存 -128 到 127 之间的数据。
public class Test { @SneakyThrows public static void main(String[] args) { Integer i1 = Integer.valueOf(100); Integer i2 = Integer.valueOf(100); Integer i3 = Integer.valueOf(200); Integer i4 = Integer.valueOf(200); Integer i5 = 100; Integer i6 = 200; System.out.println("i1与i2是否为同一对象:" + (i1 == i2)); System.out.println("i3与i4是否为同一对象:" + (i3 == i4)); System.out.println("i1与i5是否为同一对象:" + (i1 == i5)); System.out.println("i3与i6是否为同一对象:" + (i3 == i6)); } }
(2)同时在 Java 中,String 类使用了享元模式来维护字符串常量池。字符串常量池是一个特殊的存储区域,用于保存所有的字符串常量。在 Java 中,所有的字符串常量(例如 "hello")都会被保存在字符串常量池中。如果一个字符串变量的值与字符串常量池中的某个字符串相同,那么该字符串变量会指向字符串常量池中的那个字符串。这样的设计避免了在创建 N 多相同对象时所产生的不必要的大量的资源消耗。
public class Test { @SneakyThrows public static void main(String[] args) { String s1 = "abc"; String s2 = "abc"; String s3 = "123"; String s4 = s2 + s3; String s5 = "abc" + "123"; String s6 = "abc123"; System.out.println("s1与s2地址是否相同:" +(s1 == s2)); System.out.println("s4与s6地址是否相同:" +(s4 == s6)); System.out.println("s5与s6地址是否相同:" +(s5 == s6)); } }
(3)而当通过 String s = new String("abc") 这种方式创建字符串的时候,因为通过 new 方法创建对象是直接在堆内存中分配空间用来创建字符串的,详细过程分为如下两步。因此使用 == 进行两个堆内存地址比较,或者堆内存地址与常量池地址比较都是为 false。
- 检查字符串常量池中是否有 "abc" 字符串,如果没有则在常量池中创建 "abc",有则不创建
- 在堆中分配空间用来创建 "abc" 对象并且将该内存地址赋值给 s
public class Test { @SneakyThrows public static void main(String[] args) { String s1 = new String("abc"); String s2 = new String("abc"); String s3 = "abc"; System.out.println("s1与s2地址是否相同:" +(s1 == s2)); System.out.println("s1与s3地址是否相同:" +(s1 == s3)); } }
(4)由于使用 new 方式会开辟两块堆内存空间,并且也会对字符串共享产生问题。String 还提供了 intern() 方法用于手动将字符串加入常量池中,原理如下:
- 如果在当前类的常量池中存在与调用 intern() 方法的字符串等值的字符串,就直接返回常量池中相应字符串的引用;
- 否则在常量池中复制一份该字符串(Jdk7 中会直接在常量池中保存当前字符串的引用),并将其引用返回;
(5)因此,只要是堆中等值的 String 对象,使用 intern() 方法返回的都是常量池中同一个 String 引用,所以,这些等值的 String 对象通过 intern() 后使用 == 是可以匹配的。
注意:虽然 intern() 方法有许多优点:执行速度非常快,直接使用 == 进行比较要比使用 equals() 方法快很多;内存占用少。虽然 intern() 方法的优点看上去很诱人,但由于 intern() 操作每次都需要与常量池中的数据进行比较以查看常量池中是否存在等值数据,同时 JVM 需要确保常量池中的数据的唯一性,这就涉及到加锁机制,这些操作都是有需要占用 CPU 时间的,所以如果进行 intern 操作的是大量不会被重复利用的 String 的话,则有点得不偿失。由此可见,String.intern() 主要适用于只有有限值,并且这些有限值会被重复利用的场景,如数据库表中的列名、人的姓氏、编码类型等。
public class Test { @SneakyThrows public static void main(String[] args) { String s1 = new String("abc"); String s2 = new String("abc").intern(); String s3 = s1.intern(); String s4 = "abc"; System.out.println("s1与s2地址是否相同:" +(s1 == s2)); System.out.println("s2与s3地址是否相同:" +(s2 == s3)); System.out.println("s3与s4地址是否相同:" +(s3 == s4)); } }
附二:数据库连接池中的享元模式
(1)在数据库连接池的实现中,可以通过使用享元模式来共享数据库连接,从而减少创建和销毁数据库连接的次数,提高系统的效率。
(2)在数据库连接池的实现中,通常会维护一个连接池,用于存储可以被重复使用的数据库连接。当应用程序需要使用数据库连接时,会从连接池中取出一个空闲的连接,使用完后再将连接返回到连接池中。这样就可以有效地管理数据库连接,避免因为数据库连接数量过多而导致系统资源耗尽的问题。
注意:并不是所有的数据库连接池实现都使用了享元模式,还有一些实现可能使用其他方法来管理数据库连接。
全部评论(0)