享元模式和委派模式
享元模式
享元模式的意图是复用对象(对象的不可变部分),节省内存。属于结构型模式
当系统需要在多个地方使用相同的信息(对象)的时候,可以将这个对象缓存起来,这样就可以避免创建多个相同的对象,节省内存
享元对象其实可以看做是工厂模式的一种改进,它也会创建多个对象,只是会将这些对象都缓存起来,便于复用.
其代码实现⾮常简单,主要是通过⼯⼚模式,在⼯⼚类中,通过⼀个 Map 或者 List来缓存已经创建好的享元对象,以达到复⽤的⽬的。之前工厂模式种的工厂方法模式就用到了享元模式,这里就不再赘述【设计模式】工厂模式
JDK源码中的享元模式
String中的享元模式
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = "he" + "llo";
String s4 = "he" + new String("llo");
String s5 = new String("hello");
String s6 = s4.intern();
String s7 = "h";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//true
System.out.println(s1 == s4);//false
System.out.println(s1 == s6);//true
System.out.println(s1 == s9);//false
System.out.println(s4 == s5);//false
}
String类是以final
修饰的,JVM中的字符串一般保存在字符串常量池中,java会确保同一个字符串在常量池中只有一个拷贝,这个字符串常量池在JDK6.0之前是位于永久代的,从JDK7.0中,将其从永久代取出放到了堆中。
当以字面量的形式创建String变量时,JVM会在编译期间把该字面量"hello"放到字符串常量池中,在java程序启动的时候就会加载到内存中。如果有其他相同的字面量,JVM就会返回这个字面量的引用
s3中的字面量拼接后就是"hello",JVM在编译期间就会对其进行优化,所以s1和s3的引用是一个。
s4中new String(“llo”)生成了两个对象,"llo"和new String(“llo”), 一个存在于常量池,一个存在于堆中,所以s4实际是两个对象的相加,编译器不会对其进行优化,其计算结果会存放在堆中,所以s1和s4不相等
s4.intern()方法会将一个位于堆中的字符串在运行期间动态的加入到常量池中,如果字符串常量池中有该对象的字面量则返回该字面量的引用,所以s1==s6
Integer中的享元模式
public static void main(String[] args) {
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
Integer c = Integer.valueOf(1000);
Integer d = Integer.valueOf(1000);
System.out.println(a==b);//true
System.out.println(c==d);//false
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer.valueOf的源码中对数字大小做了个判断,如果是在[-128,127]的范围内,其值是从缓存中获取的, 因为这个范围的数字的使用频率是最高的,所以jdk使用了享元模式来共享数据,提高性能。
前面说享元模式会将共享的对象缓存起来,便于复用,那么它和单例,缓存,对象池有哪些区别呢?
享元模式跟单例的区别
在单例模式中,⼀个类只能创建⼀个对象,⽽在享元模式中,⼀个类可以创建多个对象,每个
对象被多处代码引⽤共享。尽管从代码实现上来看,享元模式和单例模式有很多相似之处,但从设计意图上来
看,它们是完全不同的。使用享元模式是为了对象复⽤,节省内存,⽽应⽤单例模式是为了限制对象的个数。
享元模式跟缓存的区别
在享元模式的实现中,我们通过⼯⼚类来“缓存”已经创建好的对象。这⾥的“缓存”实际上是“存储”的意思,跟我们平时所说的数据库缓存,redis缓存是两回事。我们平时所讲的缓存,主要是为了提⾼访问效率,⽽⾮复⽤
享元模式跟连接池的区别
连接池(⽐如数据库连接池)、线程池等也是为了复⽤,那它们跟享元模式有什么区别呢?
虽然连接池、线程池、享元模式都是为了复⽤,但是池化技术中的“复⽤”和享元模式中的“复⽤”实际上是不同的概念。
池化技术中的“复⽤”可以理解为“重复使⽤”,主要⽬的是节省时间(⽐如从数据库池中取⼀个连接,不需要重新创建)。在任意时刻,每⼀个连接、线程,并不会被多处使⽤,⽽是被⼀个使⽤者独占,当使⽤完成之后,放回到池中,再由其他使⽤者重复利⽤。享元模式中的“复⽤”可以理解为“共享使⽤”,在整个⽣命周期中,都是被所有使⽤者共享的,主要⽬的是节省空间。
委派模式
委派模式属于行为型模式,但不属于23种设计模式中的任何一种。它的基本作用就是负责任务调度和任务分配,可以看作是一种特殊情况下的静态代理的全权代理
有两个类A和类B,他们之间没有任何关联,但是B具有和A一模一样的方法和属性,并且调用B中的方法、属性就是调用A中同名的方法和属性。B好像就是一个受A委托的中介。第三方的代码不需要知道A的存在,也不需要和A发生直接的联系,通过B就可以直接使用A的功能,这样既能够使用到A的各种功能,又能够很好的将A保护起来,一举两得。
通俗理解:我们在项目开发中都是一个项目团队,老板把任务交给项目经理后,项目经理制定项目计划、将任务下发到底下的开发人员,这就是委派模式。但是我们发现这个跟之前学的代理模式非常相似,其实这也可以当做是静态代理模式的一种特例。此外,项目经理接到任务后会做一个权衡,怎么去选择分配这些任务。我们又发现这个跟之前学的策略模式非常相似,其实这也可以当做是策略模式的一种特例。(其实设计模式一般都不会独立存在,都是混合使用)
委派模式跟静态代理模式以及策略模式的区别?
委派模式:代理人全权负责这一件事。如:老板给项目经理安排任务,项目经理只是负责调度工作,真正干活的是底下的开发人员。
静态代理模式:代理人只是参与被代理人一小部分的工作,最终代理人还是要参与工作的。如:我需要中介帮我找房子,但是最后看房还是需要本人去看的。
委派模式主要有3个角色:
抽象任务(ITask):定义一个抽象接口
委派者(Delegate):负责在各个具体任务实例之间选择执行任务的实例,由其分派任务
具体任务(Concrete):真正执行任务的对象。
代码示例
以项目团队展开工作为例,实现如下
public interface IEmployee {
void work(String work);
}
public class CodeEmployee implements IEmployee {
@Override
public void work(String work) {
System.out.println("我做coding工作");
}
}
public class UIEmployee implements IEmployee {
@Override
public void work(String work) {
System.out.println("我做ui工作....");
}
}
public class PMEmployee implements IEmployee{
private static Map<String, IEmployee> map = new HashMap<>();
static {
map.put("画图", new UIEmployee());
map.put("接口开发", new CodeEmployee());
}
@Override
public void work(String work) {
if (map.containsKey(work)) {
map.get(work).work(work);
}
}
}
public class Test {
public static void main(String[] args) {
new Boss().commend("画图", new PMEmployee());
}
static class Boss {
public void commend(String work, PMEmployee pmEmployee) {
pmEmployee.work(work);
}
}
}
委派模式在源码中的应用
委派模式在spring源码中使用到很多,最经典的就是DispatcherServlet,根据不同的url将其交给不同的处理器去处理
我们的JVM在加载类时也使用到了双亲委派模型。一个类加载器在加载类的时候,先把这个请求委派给自己的父类加载器去执行,如果父类加载器还存在父类加载器,则继续向上委派,直到分派给顶层的启动类加载器。如果父类加载器可以完成类加载,那么就返回成功,自己需要干任何事情;只有父加载器无法完成加载,子加载器才会尝试自己去加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//尝试让父加载器去加载class
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}