点击进入我的博客
2.1 简单工厂模式
2.1.1 工厂模式的几种形态
工厂模式主要用一下几种形态:
- 简单工厂(Simple Factory):专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。它又称为静态工厂方法模式。
- 工厂方法(Factory Method):提前定义用于创建对象的接口,让子类决定实例化具体的某一个类,即在工厂和产品中间增加接口,工厂不再负责产品的创建,由接口针对不同条件返回具体的类实例,由具体类实例去实现。又称为多态性工厂模式或虚拟构造子模式。
- 抽象工厂(Abstract Factory):抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。又称为工具箱模式。
2.1.2 简单工厂模式
简单工厂模式
- 简单工厂模式(Simple Factory Pattern)又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。
- 在简单工厂模式中,可以根据自变量的不同返回不同类的实例。
- 简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
简单工厂模式的三个角色
- 工厂类(Creator)角色:担任这个角色的是工厂方法模式的核心,含有与应用紧密相关的商业逻辑。工厂类在客户端的直接调用下创建产品对象,它往往由一个具体Java类实现。
- 抽象产品(Product)角色:担任这个角色的类是工厂方法模式所创建的对象的父类,或它们共同拥有的接口。抽象产品角色可以用一个Java接口或者Java抽象类实现。
- 具体产品(Concrete Product)角色:工厂方法模式所创建的任何对象都是这个角色的实例,具体产品角色由一个具体Java 类实现。
abstract class Fruit {}
class Apple extends Fruit {}
class Banana extends Fruit {}
class FruitFactory {
public static Fruit newInstance(Class extends Fruit> clz) {
try {
return clz.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
多个工厂方法
每个工厂类可以有多于一个的工厂方法,分别负责创建不同的产品对象。比如java.text.DateFormat
类是其子类的工厂类,而它就提供了多个静态工厂方法。
工厂角色与抽象产品角色合并
在有些情况下,工厂角色可以由抽象产品角色扮演。典型的应用就是java.text.DateFormat
类,一个抽象产品类同时是子类的工厂。
三个角色全部合并
如果抽象产品角色已经被忽略,而工厂角色就可以与具体产品角色合并。换言之,一个产品类为自身的工厂。
class ConcreteProduct {
public static ConcreteProduct factory() {
return new ConcreteProduct();
}
}
2.1.3 简单工厂模式与其他模式的关系
单例模式
- 单例模式使用了简单工厂模式。换言之,单例类具有一个静态工厂方法提供自身的实例。
- 单例模式并不是简单工厂模式的退化情形,单例模式要求单例类的构造方法是私有的,从而客户端不能直接将之实例化,而必须通过这个静态工厂方法将之实例化
- 单例类自身是自己的工厂角色。换言之,单例类自己负责创建自身的实例。
- 单例类使用一个静态的属性存储自己的惟一实例 ,工厂方法永远仅提供这一个实例。
多例模式
- 多例模式是对单例模式的推广。多例模式与单例模式的共同之处在于它们都禁止外界直接将之实例化,同时通过静态工厂方法向外界提供循环使用的自身的实例。它们的不同在于单例模式仅有一个实例,而多例模式则可以有多个实例。
- 多例模式往往具有一个聚集属性,通过向这个聚集属性登记已经创建过的实例达到循环使用实例的目的。一般而言,一个典型的多例类具有某种内部状态,这个内部状态可以用来区分各个实例;而对应于每一个内部状态,都只有一个实例存在。
- 根据外界传入的参量,工厂方法可以查询自己的登记聚集,如果具有这个状态的实例已经存在,就直接将这个实例提供给外界;反之,就首先创建一个新的满足要求的实例,将之登记到聚集中,然后再提供给客户端。
备忘录模式
- 单例和多例模式使用了一个属性或者聚集属性来登记所创建的产品对象, 以便可以通过查询这个属性或者聚集属性找到和共享已经创建了的产品对象。这就是备忘录模式的应用。
MVC模式
- 简单工厂模式所创建的对象往往属于一个产品等级结构,这个等级结构可以是MVC模式中的视图(View);而工厂角色本身可以是控制器(Controller)。一个MVC 模式可以有一个控制器和多个视图,如上图所示。
- 上图中的Controller(控制器)也就是工厂角色,它负责创建产品View(视图)。
- 如果系统需要有多个控制器参与这个过程的话,简单工厂模式就不适用了,应当考虑使用工厂方法模式。
2.1.4 简单工厂模式的优点和缺点
简单工厂模式的优点
- 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象。
- 客户端无需知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
- 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
简单工厂模式的缺点
- 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
- 使用简单工厂模式将会增加系统中类的个数,在一定程序上增加了系统的复杂度和理解难度。
- 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
- 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
简单工厂模式适用环境
- 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
- 客户端只知道传入工厂类的参数,对于如何创建对象不关心;客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。
2.2 工厂方法模式
2.2.1 工厂方法模式简介
- 工厂方法模式是类的创建模式,又叫做虚拟构造子模式或多态性工厂模式。
- 在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。该核心类成为一个抽象工厂角色,仅负责给出具体工厂子类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。
2.2.2 工厂方法的结构
- 抽象工厂(Creator)角色:担任这个角色的是工厂方法模式的核心,它是与应用程序无关的。任何在模式中创建对象的工厂类必须实现这个接口。在实际的系统中,这个角色也常常使用抽象类实现。
- 具体工厂(Concrete Creator)角色:担任这个角色的是实现了抽象工厂接口的具体JAVA类。具体工厂角色含有与业务密切相关的逻辑,并且受到应用程序的调用以创建导出类。
- 抽象产品(Product)角色:工厂方法模式所创建的对象的超类,也就是所有产品对象的共同父类或共同拥有的接口。在实际的系统中,这个角色也常常使用抽象类实现。
- 具体产品(Concrete Product)角色:这个角色实现了抽象产品(Product)角色所声明的接口,工厂方法模式所创建的每一个对象都是某个具体产品角色的实例。
abstract class Fruit {}
abstract class FruitFactory {
public abstract Fruit newInstance();
}
class Apple extends Fruit {}
class Banana extends Fruit {}
class AppleFactory extends FruitFactory {
@Override
public Fruit newInstance() {
return new Apple();
}
}
class BananaFactory extends FruitFactory {
@Override
public Fruit newInstance() {
return new Banana();
}
}
2.2.3 工厂方法模式的细节
Java中的应用
java.util.Collection
接口继承来Iterable
接口,所有其子类都必须实现Iterator
方法,这个iterator()
方法就是一个工厂方法。
使用接口或者抽象类
抽象工厂角色和抽象产品角色都可以选择由Java接口或者Java抽象类来实现。
如果具体工厂角色由共同的逻辑,那么这些共同的逻辑就可以向上移动到抽象工厂角色中,这也意味着抽象工厂角色应该由抽象类实现;反之就应当由接口实现。
使用多个工厂方法
抽象工厂角色可以规定出多于一个的工厂方法,从而使具体工厂角色实现这些不同的工厂方法。
工厂方法模式的优点
- 隐藏细节:在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
- 多态性设计:工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。基于工厂角色和产品角色的多态性设计是工厂方法模式的关键,它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。
- 完全符合开闭原则:在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。
工厂方法模式的缺点
- 类数量太多:在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
- 系统的抽象性和复杂性:由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
模式适用环境
- 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
- 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
- 动态指定:将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
2.2.4 工厂方法模式与其他模式
简单工厂模式
- 工厂方法模式和简单工厂模式在结构上的不同很明显。工厂方法模式的核心是一个抽象工厂类,而简单工厂模式把核心放在一个具体类上。
- 如果系统需要加入一个新的导出类型,那么所需要的就是向系统中加入一个这个导出类以及所对应的工厂类。没有必要修改客户端,也没有必要修改抽象工厂角色或者其他已有的具体工厂角色。对于增加新的导出类型而言,这个系统完全支持“开-闭原则”。
模板方法模式
工厂方法模式常常与模版方法模式一起联合使用。原因其实不难理解:第一,两个模式都是基于方法的,工厂方法模式是基于多态性的工厂方法的,而模版方法模式是基于模版方法和基本方法的;第二,两个模式都将具体工作交给子类。工厂方法模式将创建工作推延给子类,模版方法模式将剩余逻辑交给子类。
MVC模式
工厂方法模式总是涉及到两个等级结构中的对象,而这两个等级结构可以分别是MVC中的控制器和试图。一个MVC模式可以有多个控制器和多个视图。
如果系统内只需要一个控制器,那么可以简化为简单工厂模式。
享元模式
享元模式使用了带有循环逻辑的工厂方法。
2.3 抽象工厂模式
2.3.1 抽象工厂模式简介
- 抽象工厂模式是所有形态的工厂模式中最为抽象和具有一般性的形态。
- “抽象”来自“抽象产品角色”,“抽象工厂”就是抽象产品角色的工厂。
- 抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构。
2.3.2 抽象工厂方式结构
- 抽象工厂(Creator)角色:担任这个角色的是抽象方法模式的核心,它是与应用程序无关的。
- 具体工厂(Concrete Creator)角色:具体工厂角色含有与业务密切相关的逻辑,并且受到应用程序的调用以创建导出类。
- 抽象产品(Product)角色:抽象方法模式所创建的对象的超类,也就是所有产品对象的共同父类或共同拥有的接口。
- 具体产品(Concrete Product)角色:抽象工厂模式所创建的每一个对象都是某个具体产品角色的实例。
2.3.3 抽象工厂方式细节
抽象方法模式场景
- 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节。这对于所有形态的工厂模式都是重要的;
- 一个系统的产品有多于一个的产品族,而系统只消费其中某一族的产品;
- 同属于同一个产品族的产品是在一起使用的,这一约束必须要在系统的设计中体现出来;
- 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于实现。
抽象方法模式优点
- 隔离了具体类的生成,使得用户不需要知道什么被创建了。
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
抽象方法模式缺点
- 抽象工厂的接口确定了可以被创建的产品集合,所以难以扩展抽象工厂以生成新种类的产品。
2.3.4 三种工厂模式总结
下面例子中,手机、电脑是抽象产品,苹果、三星等是工厂。
简单工厂模式
- 抽象产品叫手机
- 具体产品是苹果手机、三星手机
- 工厂有一个生产手机的方法,可以根据传入品牌是苹果还是三星决定生产哪个品牌的手机
工厂方法模式
- 抽象产品叫手机
- 具体产品是苹果手机、三星手机
- 抽象工厂叫手机工厂
- 具体工厂是苹果手机工厂和三星手机工厂,分别生产苹果手机和三星手机
抽象工厂模式
- 抽象产品叫手机、电脑
- 具体产品是苹果手机、苹果电脑、三星手机、三星电脑
- 抽象工厂叫手机电脑工厂,有两个方法分别是生产手机和生产电脑
- 具体工厂是苹果工厂和三星工厂,苹果工厂的两个方法分别生产苹果手机和苹果电脑,三星工厂的两个方法分别生产三星手机和三星电脑
2.4 单例模式
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
2.4.1 单例模式细节
核心代码
私有化构造方法!
解决什么问题
- 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 主要解决一个全局使用的类频繁地创建与销毁的问题。
单例模式的应用
- 属性文件
- Java.lang.Runtime对象
单例模式优点
- 在内存中只有一个实例,减少内存开支,特别是一个对象需要频繁地创建销毁时。
- 单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。
单例模式缺点
- 由于私有化了构造方法,所以不能继承
- 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
- 特别要注意单例对象如果持有
Context
,那么容易引发内存泄漏,此时需要注意传递给单例对象的context
,最好是Application Context
2.4.2 饿汉式与懒汉式
饿汉式
- 加载类的时候比较慢
- 运行时获得对象的速度比较快
- 它从加载到应用结束会一直占用资源。
class EagerSingleton {
// 创建单例类对象
private static EagerSingleton instance = new EagerSingleton();
// 构造方法私有化
private EagerSingleton() {}
// 获取可用对象
public static EagerSingleton getInstance() {
return instance;
}
}
懒汉式
- 是运行时获得对象的速度比较慢
- 加载类的时候比较快
- 它在整个应用的生命周期只有一部分时间在占用资源。
class LazySingleton {
// 声明单例类对象
private static LazySingleton instance;
// 构造方法私有化
private LazySingleton() {}
// 获取可用对象
public static synchronized LazySingleton getInstance() {
if(null == instance) {
instance = new LazySingleton();
}
return instance;
}
}
2.4.3 懒汉式与双重检查成例
- 由于2.4.2懒汉式代码中,直接对整个
getInstance()
方法进行了同步处理,可能会导致一些性能问题,于是有了下面的改进方法,通过双重检查和同步代码块的形式来处理懒汉式的并发问题。但要注意的是,这在Java语言中可能是问题的,之所以是可能有问题,是因为不同Java版本的内存模型不同。 - 在第一次检查时,可能会有多个线程同时到达(1)处。假设线程1和线程2都到达(1)进行第一次检查,此时
instance
为null
,两个线程都通过第一次检查 - 然后由于同步代码块加锁,只能有一个线程获取锁。线程1获取锁并向下继续执行,此时
instance
仍然为null
,于是执行(5)初始化instance = new Singleton()
,然后线程1执行完毕释放锁。 - 然后线程2获取锁,此时第二次检查判断
instance
不为null
,所以线程2不会进行初始化,直接退出,返回已经初始化好的instance
。 - 以上步骤听起来是没有问题的,但问题出在
instance = new Singleton()
这一句话并不是原子操作!
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() throws Exception {
if(null == instance) { // (1)第一次检查
// (2)这里会有多个线程同时到达
synchronized(Singleton.class) { // 同步代码块加锁
// (3)此处只能是单线程
if (null == instance) { // (4)第二次检查
instance = new Singleton(); // (5)初始化instance
}
}
}
return instance;
}
}
问题出现的原因:无序写入
为展示问题出现的原因,假设代码行instance =new Singleton();
执行了下列伪代码:
mem = allocate(); // (1)为单例对象分配内存空间.
instance = mem; // (2)注意,instance引用现在已经不是null,但还未初始化
ctorSingleton(instance); // (3)为单例对象通过instance调用构造函数
上述伪代码中,执行的顺序可能是(1)(3)(2),此时不会导致上述问题;但如果(1)(2)(3)的执行过程,则可能在线程1执行到(2)时,CPU开始执行线程2,此时恰好线程2执行到第一次检查,获取到的是一个不为null
但尚未初始化的值,此时程序会抛出错误。
使用volatile
在高版本的JDK中,使用volatile
关键字可以保证不会产生上述问题。被volatile
所修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存来实现,从而确保多个线程能正确的处理该变量。
该关键字可能会屏蔽掉虚拟机中的一些代码优化,所以其运行效率可能不是很高。
class Singleton {
private static volatile Singleton instance;
}
使用内部类
class Singleton {
private Singleton() {}
private static class Holder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
// 外围类能直接访问内部类(不管是否是静态的)的私有变量
return Holder.instance;
}
}
更多资料
- 单例模式与双重检测
- 双重检查的缺陷
- 用happen-before规则重新审视DCL
2.5 多例模式
多例模式实际上就是单例模式的推广,多例类可以有多个实例,多例类必须自己创建、管理自己的实例,并向外界提供自己的实例。
多例模式分为有上限多例类和无上限多例类,无上限多例类要通过集合来实现。
2.6 建造者模式
建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。它提供了一种创建对象的最佳方式。
2.6.1 建造者结构
- Builder(抽象建造者):可以是一个抽象类或一个接口,规范产品对象的各个组成部分的建造。
- ConcreteBuilder(具体建造者):它实现了Builder接口,给出一步一步的创建产品实例的操作,然后提供一个方法返回创建好的复杂产品对象。
- Product(产品角色):如果是单个产品类,那么就是一个具体的产品;如果是多个产品类,那么就是一个抽象的类或接口。
- ConcreteProduct(具体产品):当多个产品类时,继承抽象Product,也就是具体的要建造的复杂对象。值得注意的是,这些产品类不一定会有共同的接口。
- Director(指挥者):它复杂安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在Director的方法中调用建造者对象的部件构造与装配方法,完成建造复杂对象的任务。
2.6.2 建造者模式细节
主要目的
一个产品通常有不同的组成成分作为产品的零件,不同的产品可以有不同的零件,建造产品的过程是建造零件的过程。建造者模式将产品的结构和产品的零件建造过程对外隐藏起来,把对建造过程进行指挥的责任和具体建造零件的责任分割开来,达到责任划分和封装的目的。
解决问题
主要解决在开发过程中,有时需要创建一个复杂对象,通常由多个部分的子对象构成;由于复杂对象的多样性,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法需要保持稳定。
使用场景
- 肯德基的产品很多,需要组成“套餐”。
- Java的StringBuilder
省略角色
- 省略抽象建造者:如果只需要一个具体建造者,则可以省略抽象建造者。
- 省略指挥者:可以在具体建造者里边直接构造具体产品。
- 合并具体建造者和具体产品:在产品本身就是自己的建造者。
优点
- 良好的封装性
- 具体建造类之间独立,扩展性好
缺点
- 如果产品比较多,可能会有很多的建造类。
2.6.3 肯德基套餐案例
public class Waiter {
public static void main(String[] args) {
KFCBuilder builder = new MexicanTwisterBuilder();
builder.buildBeverage();
builder.buildHamburger();
builder.buildSnack();
KFCCombo combo = builder.getCombo();
}
}
// 套餐接口
abstract class KFCCombo {
private String hamburger;
private String beverage;
private String snack;
// getters & setters
}
// 墨西哥鸡肉卷套餐
class MexicanTwisterCombo extends KFCCombo {}
// Builder接口
interface KFCBuilder {
void buildHamburger();
void buildBeverage();
void buildSnack();
KFCCombo getCombo();
}
class MexicanTwisterBuilder implements KFCBuilder {
private KFCCombo combo = new MexicanTwisterCombo();
@Override
public void buildHamburger() {
combo.setHamburger("Mexican Twister");
}
@Override
public void buildBeverage() {
combo.setBeverage("Pepsi Cola");
}
@Override
public void buildSnack() {
combo.setSnack("Hot Wings");
}
@Override
public KFCCombo getCombo() {
return combo;
}
}
2.6.4 builder内部类
如果一个类有很多属性,此时为此类写一个Builder内部类,来辅助建造该类。
class Phone {
private String screen;
private String camera;
private String cpu;
private String battery;
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Phone phone = new Phone();
public Builder screen(String screen) {
phone.screen = screen;
return this;
}
public Builder camera(String camera) {
phone.camera = camera;
return this;
}
public Builder cpu(String cpu) {
phone.cpu = cpu;
return this;
}
public Builder battery(String battery) {
phone.battery = battery;
return this;
}
public Phone build() {
return phone;
}
}
}
2.6.5 与其他模式的关系
抽象工厂模式
- 抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。
- 而建造者模式则是要求按照规定建造产品,它的主要目的是通过组装零配件而产生一个新产品。
- 换言之,抽象工厂模式在更加具体的维度上,而建造模式在一个更加宏观的维度上。
策略模式
- 事实上建造模式是策略模式的一种特殊情况,这两种模式的却别在于用意不同。
- 建造模式适应于为客户端一点一点地建造新的对象。
- 策略模式的目的是为算法提供抽象的接口。
2.7 原始模型模式
原始模型模式通过给一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象。
2.7.1 原型模式结构
这种模式涉及到三个角色:
- 客户(Client)角色:客户类提出创建对象的请求
- 抽象原型(Prototype)角色:这是一个抽象角色,此角色给出所以的具体原型类所需的接口。
- 具体原型(Concrete Prototype):被复制的对象。
2.7.2 原型模式细节
主要目的
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
适用环境
- 创建新对象成本较大(例如初始化时间长,占用CPU多或占太多网络资源),新对象可以通过复制已有对象来获得,如果相似对象,则可以对其成员变量稍作修改。
- 系统要保存对象的状态,而对象的状态很小。
- 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的组合状态,通过复制原型对象得到新实例可以比使用构造函数创建一个新实例更加方便。
优点
- 当创建对象的实例较为复杂的时候,使用原型模式可以简化对象的创建过程,通过复制一个已有的实例可以提高实例的创建效率。
- 扩展性好,由于原型模式提供了抽象原型类,在客户端针对抽象原型类进行编程,而将具体原型类写到配置文件中,增减或减少产品对原有系统都没有影响。
- 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式不需要这样,圆形模式中产品的复制是通过封装在类中的克隆方法实现的,无需专门的工厂类来创建产品。
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
缺点
- 需要为每一个类配置一个克隆方法,而且该克隆方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
- 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重签到引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。
2.7.3 Java的Clone
Object.clone()
clone()
方法返回的对象叫做原始对象的克隆体。一个克隆对象的基本特性必须是:
-
a.clone()!=a
,这也就意味着克隆对象和原始对象在java中是两个不同的对象。 -
a.clone().getClass == a.getClass()
,克隆对象与原对象类型相同 -
a.clone.equals(a)
,也就是说克隆对象完完全全是原始对象的一个拷贝。此条件是非必需的。
Cloneable接口
Object
类没有实现该接口,所以用户如果没有主动实现该接口时,调用clone()
方法会报错CloneNotSupportedException
。
Java实现步骤
- 实现
Cloneable
接口,这是步骤的关键之处。 - 重写
clone()
方法,并声明为public
,因为Object
的该方法是protected
的。 - 调用
super.clone()
来获取新的克隆对象。在运行时刻,Object
中的clone()
识别出你要复制的是哪一个对象,然后为此对象分配空间,并进行对象的复制,将原始对象的内容一一复制到新对象的存储空间中。
class A implements Cloneable {
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
2.7.4 深复制和浅复制
浅复制
- 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。
-
Object.clone()
是浅复制。
深复制
- 被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
- 深复制要深入到多少层是一个不易确定到问题,需要特别注意
- 深复制的过程中可能出现循环引用到问题,需要小心处理
利用串行化进行深复制
- 把对象写到流里的过程是串行化(Serilization)过程,但是在Java程序师圈子里又非常形象地称为“冷冻”或者“腌咸菜(picking)”
- 把对象从流中读出来的并行化(Deserialization)过程则叫做 “解冻”或者“回鲜(depicking)”过程。
- 应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此“腌咸菜”的只是对象的一个拷贝,Java咸菜还可以回鲜。
- 利用这个特性,在Java语言里深复制一个对象,可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以克隆对象。
- 这样做的前提是对象以及对象内部所有引用到的对象都是可串行化的,否则,就需要仔细考察那些不可串行化的对象可否设成
transient
,从而将之排除在复制过程之外。
public Object deepClone() throws Exception {
//将对象写到流里
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(this);
//从流里读出来
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return(oi.readObject());
}