GOF23 享元模式
享元模式
享元模式概述
面向对象技术可以很好的解决一些灵活性或可扩展性的问题,但在很多情况下需要再系统中值增加类和对象个数。当对象数量太多时,将导致运行代价过高,带来性能下降问题。享元模式正是为解决这个问题而生的
享元模式的通用写法
public class Client {
public static void main(String[] args) {
FlyweightFactory flyweightFactory = new FlyweightFactory();
IFlyweight flyweight1 = flyweightFactory.getFlyweight("aa");
IFlyweight flyweight2 = flyweightFactory.getFlyweight("bb");
flyweight1.operation("a");
flyweight2.operation("b");
}
//抽象享元角色
interface IFlyweight{
void operation(String extrinsicState);
}
//具体享元角色
static class ConcreteFlyweight implements IFlyweight{
private String intrinsicState;
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
@Override
public void operation(String extrinsicState) {
System.out.println("Object address: "+System.identityHashCode(this));
System.out.println("IntrinsicState: "+this.intrinsicState);
System.out.println("ExtrinsicState: "+extrinsicState);
}
}
//享元工厂
static class FlyweightFactory{
private static Map<String,IFlyweight> pool = new HashMap<>();
//因为内部状态具体不变性,所以作为缓存的键
public static IFlyweight getFlyweight(String intrinsicState){
if (!pool.containsKey(intrinsicState)){
IFlyweight flyweight = new ConcreteFlyweight(intrinsicState);
pool.put(intrinsicState,flyweight);
}
return pool.get(intrinsicState);
}
}
}
解决实际购票问题
- ITicket接口
public interface ITicket {
void showInfo(String bunk);
}
TrainTicket
接口
public class TrainTicket implements ITicket {
private String from;
private String to;
private int price;
public TrainTicket(String from, String to) {
this.from = from;
this.to = to;
}
@Override
public void showInfo(String bunk) {
this.price= new Random().nextInt(500);
System.out.printf("%s->%s: %s 价格: %d元\n",this.from,this.to,bunk,this.price);
}
}
- TicketFactory类
public static ITicket queryTicket(String from,String to){
return new TrainTicket(from,to);
}
public static void main(String[] args) {
ITicket iTicket = TicketFactory.queryTicket("北京", "上海");
iTicket.showInfo("软卧");
}
由上面的构造工厂代码所知,当客户端进行查询的时候,系统通过TicketFactory直接创建一个火车票对象,但是这样做的话,当某个瞬间如果有大量用户查询一张票的信息时,系统就会创建出大量该火车的对象,内存压力暴增。
所以我们对代码进行更改
private static Map<String,ITicket> sTicketPool = new ConcurrentHashMap<>();
public static ITicket queryTicket(String from,String to){
String key = from + "->" +to;
if (TicketFactory.sTicketPool.containsKey(key)){
System.out.println("使用缓存: "+key);
return TicketFactory.sTicketPool.get(key);
}
System.out.println("首次查询:"+key);
ITicket ticket = new TrainTicket(from,to);
TicketFactory.sTicketPool.put(key,ticket);
return ticket;
}
再次进行测试
享元模式在JDK源码中的应用
String中的享元模式
java
中将String类定义为由final修饰的,JVM
中字符串一般被保护在字符串常量池中,java
会确保一个字符串在常量池中只有一个“复制”,这个字符串常量池在JDK6.0
以前是位于常量池中的,位于永久代;而JDK7.0
,JVM
将其从永久代拿出来放置于堆中
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = "he"+"llo";
String s4 = "hel"+new String("lo");
String s5 = new String("hello");
String s6 = s5.intern();
String s7 = "h";
String s8 = "ello";
String s9 = s7+s8;
System.out.println(s1==s2);
System.out.println(s1==s3);
System.out.println(s1==s4);
System.out.println(s1==s9);
System.out.println(s4==s5);
System.out.println(s1==s6);
}
String类是由final修饰的,当以字面量的形式创建String变量时,JVM会在编译期间就把该字面量“hello”放在字符创常量池中,在java启动的时候就已经加载到内存中了。这个字符创常量池的特点就是有且只有一份相同的字面量。如果有其他相同的字面量,则JVM返回这个字面量的引用;如果没有相同的字面量,则在字符串常量池中创建这个字面量并返回它的引用
S5
的intern方法将一个位于堆中的对象放置于常量池在运行期间动态地放置于常量池中(字符串常量池的内容是在程序启动的时候已经加载好了)。如果字符串常量池中有该对象对应的字面量。则返回该字面量在字符串常量池中的引用;否则,复制一份该字面量到字符串常量池并返回它的引用。因此s1=s6
输出true;
Integer中的享元模式
public static void main(String[] args) {
Integer l1 = Integer.valueOf(127);
Integer b = 127;
Integer l2 = Integer.valueOf(128);
Integer c = 128;
System.out.println(l1==b);
System.out.println(l2==c);
}
之所以得到这样的结果是因为Integer用到了享元模式 接下来我们看一下它的源码
由上可知,Integer源码中的valueof
()方法做一个条件判断,如果目标值在-128~127,则直接从缓存中取值,否则新建对象。JDK
为何要这样做?因为-128 ~ 127 的数据在int范围内是使用最频繁的,为了减少频繁创建对象带来的内存消耗,这里就用到了享元模式,以提高性能
Long也用到了享元模式
和Integer相似,Long源码也用到了享元模式,将-128~127的值缓存起来
享元模式的扩展
享元模式的内部和外部状态
享元模式的定义提出了两个要求:细粒度和共享对象。因为要求细粒度,所以不可避免的会使对象的数量多且性质接近,此时我们就会将这些对象的信息分为两个部分:内部转态和外部转态。
内部状态指对象共享出来的信息,存储在享元对象的内部,并且不会随环境的改变而改变;外部转态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。
比如,连接池中的连接对象,保存在连接中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将他标记为可用状态,这些为外部转态。
优点
- 减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率
- 减少内存之外的其他资源占用
缺点
- 关注内、外部状态,关注线程安全问题
- 使系统、程序的逻辑复杂化