本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试。
记录日期:2022.1.6
大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习。
设计模式-23种设计模式之创建型模式
这部分我之前的学习,是通过博客,以及《Spring 5核心原理与30个类手写实战》这本书学习的,这本书前面会介绍设计模式多一点。
当然比较推荐的是《Java设计模式》。
比较详细的博客链接参考:史上最全设计模式导学目录(完整版)
设计模式分类
设计模式介绍
设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式具有以下优点。
- 可以提高程序员的思维能力、编程能力和设计能力。
- 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
- 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
一般主要讲一下23种设计模式。
创建型模式
创建型设计模式,是指在创建对象的同时隐藏创建逻辑,不使⽤ new 直接实例化对象,程序在判断需要创建哪些对象时更灵活。包括下面五种模式:
- 工厂方法模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
- 单例模式(Singleton)
- 建造者模式(Builder)
- 原型模式(Prototype)
一般说二四种设计模式的时候,是指23个GoF设计模式+简单工厂模式,简单工厂模式属于创建型模式,待会也可以提一下。
科普一下GOF:GOF是设计模式的经典名著Design Patterns: Elements of Reusable Object-Oriented Software(中译本名为《设计模式——可复用面向对象软件的基础》)的四位作者,他们分为是:Elich Gamma、Richard Helm、Ralph Johnson、以及John Vlissides。这四个人常被称为Gang of Four, 即四人组,简称Gof。 他们总结了23个设计模式。
结构型模式
结构型设计模式,是指通过类和接⼝间的继承和引⽤实现创建复杂结构的对象。包括下面七种模式:
-
适配器模式(Adapter)
-
装饰器模式(Decorator)
-
代理模式(Proxy)
-
外观模式(Facade)
-
桥接模式(Bridge)
-
组合模式(Composite)
-
享元模式(Flyweight)
行为型模式
行为型设计模式,是指通过类之间不同通信方式实现不同行为。包括下面十一种模式:
-
策略模式(Strategy)
-
模板方法模式(Template Method)
-
观察者模式(Observer)
-
迭代器模式(Iterator)
-
责任链模式(Chain of Responsibility)
-
命令模式(Command)
-
备忘录模式(Memento)
-
状态模式(State)
-
访问者模式(Visitor)
-
中介者模式(Mediator)
-
解释器模式(Interpreter)
上面二十三种中重点学习的设计模式我用加粗标出来,大部分是平时有用到的设计模式。
创建型模式
简单工厂模式
参考文章链接:设计模式(一)简单工厂模式
概述
简单工厂模式(Simple Factory Pattern
)又称为静态工厂方法(Static Factory Method
)模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
简单来说是指由⼀个工厂对象来创建实例,客户端不需要关注创建逻辑,只需提供传⼊工厂的参数。
模型结构图
从上图可以看出,简单工厂模式由三部分组成:具体工厂(Factory)
、具体产品(Product)
和抽象产品(AbstractProduct)
。
- 工厂类:担任这个角色的是简单工厂模式的核心,含有与应用紧密相关的商业逻辑。工厂类在客户端的直接调用下创建产品对象,它往往由一个具体Java类实现。
- 抽象产品:担任这个角色的类是由简单工厂模式所创建的对象的父类,或它们共同拥有的接口。抽象产品角色可以用一个Java接口或者Java抽象类实现。
- 具体产品:简单工厂模式所创建的任何对象都是这个角色的实例,具体产品角色由一个具体Java类实现。
示例代码
模拟汽车工厂,制造汽车的过程,目前只有宝马、奔驰、奥迪三款车型,模拟代码如下:
/** * 汽车生产工厂 */
public class CarFactory {
public static Car getCar(String carName) {
if (Objects.equals(carName, "aodi")) return new AoDi();
else if (Objects.equals(carName, "baoma")) return new BaoMa();
else if (Objects.equals(carName, "benchi")) return new BenChi();
else return null;
}
}
// 汽车接口
interface Car {
void drive();
}
// 宝马
class BaoMa implements Car {
public void drive() {
System.out.println("开宝马");
}
}
// 奔驰
class BenChi implements Car {
public void drive() {
System.out.println("开奔驰");
}
}
// 奥迪
class AoDi implements Car {
public void drive() {
System.out.println("开奥迪");
}
}
实际使用时通过CarFactory汽车工厂的getCar()方法,传入车名,即可返回车实例,测试代码如下:
public static void main(String[] args) {
// 喜提宝马
Car baoma = CarFactory.getCar("baoma");
// 开宝马
car.drive();
}
模式优点
- 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象。
- 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
- 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
- 当需要引入新的产品是不需要修改客户端的代码,只需要添加相应的产品类并修改工厂类就可以了,所以说从产品的角度上简单工厂模式是符合“开-闭”原则的。
模式缺点
- 由于工厂类集中了所有产品创建逻辑,工厂类一般被我们称作“全能类”或者“上帝类”,因为所有的产品创建他都能完成,这看似是好事,但仔细想想是有问题的。比如全国上下所有的事情都有国家主义一个人干会不会有问题,当然有!一旦不能正常工作,整个系统都要受到影响。
- 使用简单工厂模式将会增加系统中类的个数,在一定程序上增加了系统的复杂度和理解难度。
- 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。所以说从工厂的角度来说简单工厂模式是不符合“开-闭”原则的。
- 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
使用场景
- 工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
- 客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。
最佳示例
java.text.DateFormat
日期格式化对象,通过调用getDateInstance()
方法获取DateFormat
的实现类。
工厂方法模式
参考文章链接:设计模式(二)工厂方法模式
概述
工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic Factory)模式。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象
,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成
,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
模型结构图
我们可以看到工厂方法模式一共分为抽象工厂(AbstractFactory)
、具体工厂(ConcreteFactory)
、抽象产品(AbstractProduct)
、具体产品(ConcreteProduct)
四个部分:
- 抽象工厂:担任这个角色的是工厂方法模式的核心,它是与应用程序无关的。任何在模式中创建对象的工厂类必须继承或者实现这个接口,在实际的系统中,这个角色常常有Java抽象类来实现。
- 具体工厂:担任这个角色的是实现了抽象工厂接口的具体Java类。具体工厂角色含有与应用密切相关的逻辑,并且受到应用程序的调用以创建产品对象。
- 抽象产品:工厂方法模式所创建的对象的超类型,也就是产品对象的共同父类或共同拥有的接口。在实际应用中这个角色常常由Java的抽象类来实现。
- 具体产品:这个角色实现了抽象产品角色所声明的接口,工厂方法所创建的每一个对象都是某个具体产品角色的实例。
工厂方法模式和简单工厂模式的区别?
- 解决简单工厂模式中存在的“上帝类”的问题,将具体的生产任务放到子类中去。
- 解决简单工厂模式工厂角度不符合“开-闭”原则的问题。
- 解决简单工厂模式不能使用继承的问题。
示例代码
模拟汽车工厂,制造汽车的过程,目前只有宝马、奔驰、奥迪三款车型。
汽车工厂下有三个子工厂,分别是宝马汽车工厂,奔驰汽车工厂,奥迪汽车工厂,用户可以去找该工厂旗下的对应品牌子工厂提车,代码如下:
// 汽车工厂抽象类
abstract class CarFactory {
abstract Car getCar();
}
// 奔驰子工厂
class BenChiCarFactory extends CarFactory {
Car getCar() {
return new BenChi();
}
}
// 宝马子工厂
class BaoMaCarFactory extends CarFactory {
Car getCar() {
return new BaoMa();
}
}
// 奥迪子工厂
class AoDiCarFactory extends CarFactory {
Car getCar() {
return new AoDi();
}
}
实际使用时通过对应的子工厂来获取对应的车,测试代码如下:
public static void main(String[] args) {
// 宝马汽车工厂
CarFactory factory = new BaoMaCarFactory();
// 喜提宝马
Car baoma = factory.getCar();
// 开宝马
baoma.drive();
}
模式优点
- 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
- 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
- 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
模式缺点
- 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
- 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
使用场景
- 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
- 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
- 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
最佳示例
- Object中的
toString()
方法:Object仅仅定义了返回String,由Object的子类来决定如何生成这个字符串。 - 抽象类
java.util.Calendar
的getInstance()
方法将根据不同的情况返回不同的Calendar
子类的对象。
抽象工厂模式
参考文章链接:设计模式(三)抽象工厂模式
参考文章链接:抽象工厂模式(通俗易懂)
概述
抽象工厂模式(Abstract Factory Pattern),提供一个创建一系列相关或相互依赖对象的接口
,而无须指定它们具体的类。抽象工厂模式又称为Kit模式。
简单理解成为围绕一个巨型工厂创建小型工厂,这个巨型工厂又是其他工厂的工厂。
模型架构图
从上图看以看出抽象工厂模式
和工厂方法模式
类似都是由四部分组成:
- 抽象工厂:担任这个角色的是抽象工厂模式的核心,是与应用系统的商业逻辑无关的。通常使用Java接口或者抽象Java类实现。所有的具体工厂必须实现这个Java接口或继承这个抽象的Java类。
- 具体工厂:这个角色直接在客户端的调用下创建产品的实例,这个角色含有选择合适的产品对象的逻辑,而这个逻辑是与应用系统商业逻辑紧密相关的。
- 抽象产品:担任这个角色的类是抽象工厂模式所创建的对象的父类,或它们共同拥有的接口。通常使用Java接口或者抽象Java类实现这一角色。
- 具体产品:抽象工厂模式所创建的任何产品对象都是一个具体的产品类的实例。这是客户端最终需要的东西,其内部一定充满了应用系统的商业逻辑。通常使用具体Java类实现这个角色。
与工厂方法模式不同的是,抽象工厂模式中的具体工厂不再是只能创建一种产品,一个具体的工厂可以创建一个产品族的产品。
模型示例
首先理解两个概念:
- 产品族,是指一个品牌下面的所有产品。例如华为的手机、路由器、电脑等等…称为华为的产品族。
- 产品等级,是指多个品牌下面的同种产品。例如华为和小米各自的手机称为一个产品等级。
按照这种关系梳理一下逻辑,构建一下类图:
- 有手机和路由器两种产品,定义两个接口;
- 小米和华为都可以生产这两种产品,所以有4个实现类;
- 现在需要创建华为和小米的工厂类,先将工厂类进行抽象,里面有创建两个产品的方法,返回的是产品的接口类;
- 创建华为和小米的工厂实现类,继承工厂类接口,实现创建各自产品的方法;
- 客户端调用时,直接用工厂接口类创建需要的工厂,拿到对应的产品;
类图构建:
拓展产品族
拓展一个产品族是非常困难的,例如产品族中新增一个笔记本电脑,也就是说华为和小米现在可以生产电脑了,如下图所示(黄色字体为新增一个产品族需要做的事),对顶层的工厂接口类也要修改,这是非常麻烦的。
拓展产品等级
扩展一个产品等级,例如新增一个手机,也就是说新增一个品牌来生产手机,如下图所示(黄色字体为新增一个产品等级需要做的事),新增一个产品等级不用修改原来的代码,符合OCP
原则,这是非常舒服的。
模式优点
- 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易。所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。另外,应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。
- 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
模式缺点
- 在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,
要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改
,显然会带来较大的不便。 - 开闭原则的倾斜性(
增加新的工厂和产品族容易,增加新的产品等级结构麻烦
)。
使用场景
- 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有形态的工厂模式都是重要的。
- 这个系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
- 同属于同一个产品族的产品是在一起使用的,这一约束必须在系统的设计中体现出来。
- 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖与实现。
抽象工厂模式的退化
- 当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成
工厂方法模式
。 - 当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成
简单工厂模式
。
最佳示例
总结一下就是抽象工厂模式比较适合产品族变动较多,而产品等级变动较少的情况。
在java.sql.Connection
接口中有绝佳的示例:
public interface Connection extends Wrapper, AutoCloseable {
...
//返回普通的sql执行器
Statement createStatement() throws SQLException;
//返回具有参数化预编译功能的sql执行器
PreparedStatement prepareStatement(String sql) throws SQLException;
//返回可以执行存储过程的sql执行器
CallableStatement prepareCall(String sql) throws SQLException;
...
}
Connection
接口就是一个抽象工厂接口,描述了不同产品等级Statement
、PreparedStatement
和CallableStatement
,它们都位于抽象接口Statement
产品等级结构中,类图如下:
如果从MySQL
(产品族)的Connection
中获取的Statement
、PreparedStatement
和CallableStatement
肯定都是MySQL
,因为他们都是一个产品族。
而从SQL Server
中获取的肯定都是SQL Server
对应的sql执行器。
假如以后又有新的数据库出现,想在Java中使用它就需要扩展产品族并实现相关接口即可,并不需要修改原有的接口
单例模式(重点)
参考文章链接:设计模式(四)单例模式
概述
单例模式(Singleton Pattern),又名单件模式或单态模式,它是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
单例模式的要点有三个:
- 某个类只能有一个实例。
- 它必须自行创建这个实例。
- 它必须自行向整个系统提供这个实例。
模型结构图
我们可以看到单例模式与我们之前所讲的工厂模式不同,它只有一个Singleton
角色,并且这个类自行创建自己的实例,并向系统提供这个实例,可以注意到我没有在图中吧实例的建造个数限制为1个,后面会讲一下和多例模式的关系。
单例模式分为懒汉式
和饿汉式
,有的地方也会讲登记式的单例模式、静态内部类创建单例。
- 懒汉式:在真正需要使用对象时才去创建该单例类对象
- 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
饿汉式单例模式
饿汉式单例模式是Java语言中实现起来最为简单的单例模式。
饿汉式单例模式是在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
饿汉式单例模式示例代码如下:
public class Singleton{
/** * 直接创建一个本类的对象 */
private static final Singleton singleton = new Singleton();
/** * 覆盖默认的构造方法,将默认的构造方法声明为私有的,防止其他类直接创建对象 */
private Singleton(){
}
/** * 提供一个工厂方法来返回本类的唯一对象 */
public static Singleton getInstance() {
return singleton;
}
当这个类被加载时,静态变量singleton就会被初始化,这时会在堆内存中创建一个Singleton对象,这个类的私有构造方法就会被调用。
这个时候,单例类的唯一实例就被创建出来了。但需要获得单例对象就要调用getInstance()
方法。
当类被卸载时,Singleton对象也随之消亡了。
需要注意的一点就是单例类的构造方法一定要声明为私有的,否则其他类就可以利用构造方法直接创建对象,使单例类不再是只有一个唯一的实例。
另外,值得一提的是,由于构造方法是私有的,因此此类不能被继承。
懒汉式单例模式
懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化**(判空),**若已实例化直接返回该类对象,否则则先执行实例化操作。
懒汉式单例模式示例代码如下(当然在安全性上是有问题的,后面会讲):
public class Singleton {
/** * 单例类的唯一实例,但是不是加载时初始化 */
private static Singleton singleton;
/** * 覆盖原有的默认构造方法,声明为私有的,防止其他类直接使用构造方法创建单例类对象,同时也使子类无法继承 */
private Singleton(){
}
public static Singleton getInstance() {
/** * 如果实例为空就创建实例,创建的语句只会执行一次(当然这段代码是有问题的!!!!!!!!!!!!!!!!!!!) */
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
懒汉式和饿汉式的区别
懒汉式单例模式与饿汉式单例模式相比,更节省资源,但是必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作为资源控制器在实例化时必然涉及资源初始化,而资源初始化很有可能耗费时间,这就意味着出现多个线程同时首次引用此类的几率变得较大。
静态内部类加载单例
我们在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用。
静态内部类加载实例示例代码如下:
class Singleton {
private Singleton() {
}
private static class HolderClass {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return HolderClass.instance;
}
public static void main(String args[]) {
Singleton s1, s2;
s1 = Singleton.getInstance();
s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
}
由于静态单例对象没有作为Singleton
的成员变量直接实例化,因此类加载时不会实例化Singleton
,第一次调用getInstance()
时将加载内部类HolderClass
,在该内部类中定义了一个static
类型的变量instance
,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()
方法没有任何线程锁定,因此其性能不会造成任何影响。
通过使用静态内部类加载单例,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式。
登记式单例模式(了解)
登记式单例模式就是每使用一次,都往一个固定的容器中去注册并且将使用过的对象进行缓存,下次去取对象的时候,就直接从缓存中取值,以保证每次获取的对象都是同一个对象。
登记式单例模式实际对一组单例模式进行的维护,主要是在数量上的扩展,通过map我们把单例存进去,这样在调用时,先判断该单例是否已经创建,是的话直接返回,不是的话创建一个登记到map中,再返回。对于数量又分为固定数量和不固定数量的。下面采用的是不固定数量的方式,在getInstance方法中加上参数(string name)。然后通过子类继承,重写这个方法将name传进去。
登记式单例模式示例代码如下:
public class BeanFactory {
private BeanFactory(){
}
//线程安全
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getBean(String className) {
if(!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance(); // 使用反射创建
ioc.put(className,obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}else{
return ioc.get(className);
}
}
}
登记式的单例模式解决了懒汉式和饿汉式不能继承的缺点,但是子类中的构造方法变为了public的,所以其他类可以直接通过构造方法创建类的实例而不用向父类中登记,这是登记式单例模式最大的优点。
子类实现的代码如下:
public class ApplicationContext extends BeanFactory{
/** * 构造方法必须是公有的,否则父类无法产生子类的对象 */
public ApplicationContext(){
}
/** * 工厂方法,获取本类的唯一实例,实际上是借助了父类的getBean方法 * @return */
public static Object getInstance() {
return (ApplicationContext) BeanFactory.getBean("com.singleton.ApplicationContext");
}
}
Spring中的做法,就是用这种注册式单例,这块在讲Spring IOC的时候再细说。
懒汉式单例模式如何保证只创建一个对象
刚才写了一段懒汉式单例模式的问题代码如下:
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
这个方法其实是存在问题的,试想一下,如果两个线程同时判断singleton为空,那么它们都会去实例化一个Singleton对象,这就变成双例了。所以,我们要解决的是线程安全问题。
那么如何解决线程安全问题呢?下面慢慢讲一下思路。
第一步,最容易想到的解决方法就是在方法上加锁,或者是对类对象加锁
。
public static synchronized Singleton getInstance() {
// 在方法上加锁,静态方法加锁的锁对象是当前Class类对象
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
// 或者
public static Singleton getInstance() {
synchronized(Singleton.class) {
// 对类对象加锁
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
这样虽然规避了两个线程同时创建Singleton对象的风险,但是每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。
第二步,我们可以做一步优化,提供快速检查的方式,如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例。(或许这个思想可以理解下AQS入队的快速失败?面试引申一下也行)
public static Singleton getInstance() {
if (singleton == null) {
// 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) {
// 线程A或线程B获得该锁进行初始化
if (singleton == null) {
// 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}
上面的代码就解决了并发安全+性能低效问题。
因为需要两次判空,且对类对象加锁,该懒汉式写法也被称为:Double Check(双重校验) + Lock(加锁),即双重校验锁”
。
但是还有个问题,我在之前的volatile文章和对象实例化文章
中有说过,new对象不是一个原子性操作,为了保证这个单例对象的操作是原子性的,就需要添加volatile关键字。
第三步,在变量上加volatile关键字
。
public class Singleton {
private static volatile Singleton singleton;
private Singleton(){
}
public static Singleton getInstance() {
if (singleton == null) {
// 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) {
// 线程A或线程B获得该锁进行初始化
if (singleton == null) {
// 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}
}
这段代码就是最终的代码了,一定要考虑上面说的三个问题。
破坏懒汉式单例与饿汉式单例
反射和序列化都可以破坏掉单例模式的原则(即产生多个对象)。
反射破坏
示例代码如下:
public static void main(String[] args) {
// 获取类的显式构造器
Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
// 可访问私有构造器
construct.setAccessible(true);
// 利用反射构造新对象
Singleton obj1 = construct.newInstance();
// 通过正常方式获取单例对象
Singleton obj2 = Singleton.getInstance();
System.out.println(obj1 == obj2); // false
}
利用反射,强制访问类的私有构造器,去创建另一个对象。
序列化和反序列化破坏
示例代码如下:
public static void main(String[] args) {
// 创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// 将单例对象写到文件中
oos.writeObject(Singleton.getInstance());
// 从文件中读取单例对象
File file = new File("Singleton.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == Singleton.getInstance()); // false
}
readObject() 方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址。
保护单例模式
重写readResolve()方法
给Singletion重写readResolve()方法:
public class Seriable implements Serializable {
public final static Seriable INSTANCE = new Seriable();
private Seriable(){
}
public static Seriable getInstance(){
return INSTANCE;
}
private Object readResolve(){
return INSTANCE;
}
}
该方法的反序列化类,反序列化时会用readResolve方法返回的内容(浅拷贝)代替。
枚举(最优解)
在 JDK1.5 后,使用 Java 语言实现单例模式的方式又多了一种:枚举。
枚举创建单例模式示例如下:
public enum Singleton {
INSTANCE;
Singleton() {
System.out.println("枚举创建对象了"); }
public static void main(String[] args) {
test();
}
public void test() {
Singleton t1 = Singleton.INSTANCE;
Singleton t2 = Singleton.INSTANCE;
System.out.print("t1和t2的地址是否相同:" + t1 == t2);
}
}
// 枚举创建对象了
// t1和t2的地址是否相同:true
使用枚举的优点有以下几点:
- 代码对比饿汉式与懒汉式来说,更加地简洁,其次,既然是实现单例模式,那这种写法必定满足单例模式的要求,而且使用枚举实现时,没有做任何额外的处理。
- 它不需要做任何额外的操作去保证对象单一性与线程安全性。
- 使用枚举可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象,破坏单例模式。
关于第二点,我们可以理解一下枚举实现单例的过程:在程序启动时,会调用Singleton的空参构造器,实例化好一个Singleton对象赋给INSTANCE,之后再也不会实例化
。
关于第三点,我们来说一下枚举的防反射
和防序列
化原理:
- 防反射是因为:枚举类默认继承了
Enum
类,在利用反射调用newInstance()
时,会判断该类是否是一个枚举类,如果是,则抛出异常。 - 防反序列化是因为:在读入
Singleton
对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用Enum
类的valueOf(String name)
方法根据变量的名字查找对应的枚举对象。所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。
那我们来总结一下枚举创建单例对象几个重点:
- Enum 类内部使用Enum 类型判定防止通过反射创建多个对象。
- Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象。
- 枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。
模式优点
- 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
模式缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
最佳示例
Java中的Runtime对象就是一个使用单例模式的例子。在每一个Java应用程序里面,都有唯一的一个Runtime对象,通过这个对象应用程序可以与其运行环境发生相互作用。
Runtime
类提供一个静态工厂方法getRuntime()
,返回静态变量currentRuntime
。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
通过调用次方法,可以获得Runtime
类唯一的一个实例。
引申:多例模式
多例模式的特点:
- 多例可以有多个实例。
- 多例类必须能够自我创建并管理自己的实例,并且向外界提供自己的实例。
这个就直接看代码就能明白了,与单例的概念相反就对了。
代码示例
编写一个对象池,代码如下:
package com.singleton;
import java.util.Random;
public class ObjectPool {
/** * 两个类实例 */
private static Object o1 = new Object();
private static Object o2 = new Object();
/** * 私有的构造方法 */
private ObjectPool(){
}
/** * 根据用户使用的标号来决定返回哪一个对象 * @param witchOne * @return */
public static Object getInstance(Integer id){
if(Objects.equals(id, 1)) {
return o1;
} else {
return o2;
}
}
/** * 使用对象产生随机数 * @return */
public synchronized int randomId(){
Random rd = new Random();
return rd.nextInt(6) + 1;
}
}
客户端调用池子的getInstance()方法,获取实例对象:
package com.singleton;
public class Client {
public static void main(String[] args){
Object o1 = ObjectPool.getInstance(1);
Object o2 = ObjectPool.getInstance(2);
System.out.println(o1.randomId());
System.out.println(o2.randomId());
}
}
最佳示例
线程池、数据库连接池…
建造者模式
概述
建造者模式(Builder Pattern),是指将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节
。
模型结构图
从图中我们可以看出,创建者模式由抽象创建者角色
、具体创建者角色
、导演者角色
、产品角色
四部分组成:
- 抽象创建者角色:给出一个抽象接口,以规范产品对象的各个组成成分的建造。一般而言,此接口独立于应用程序的商业逻辑。模式中直接创建产品对象的是具体创建者角色。具体创建者必须实现这个接口的两种方法:一是
建造方法
,比如图中的productPart1()
和productPart2()
方法;另一种是结果返回方法
,即图中的getProduct()
方法。一般来说,产品所包含的零件数目与建造方法的数目相符。换言之,有多少零件,就有多少相应的建造方法。 - 具体创建者角色:他们在应用程序中负责创建产品的实例。这个角色要完成的任务包括:1、实现抽象创建者所声明的抽象方法,给出一步一步的完成产品创建实例的操作。 2、在创建完成后,提供产品的实例。
- 导演者角色:这个类调用具体创建者角色以创建产品对象。但是导演者并没有产品类的具体知识,真正拥有产品类的具体知识的是具体创建者角色。
- 产品角色:产品便是建造中的复杂对象。一般说来,一个系统中会有多于一个的产品类,而且这些产品类并不一定有共同的接口,而完全可以使不相关联的。
示例代码
接着之前的例子来说,一辆汽车的生产过程非常复杂,消费者在购买汽车的时候根本不需要关心汽车是怎么样造出来的,他们只关心汽车到手就好了。
但是我们会想,我们可以在Aodi类的构造方法中进行零件的生产和拼装,但是这样做的话BenChi车和BaoMa车都需要在构造方法中加入制造零件和拼装组建的功能。这样会显得的很麻烦,因为每个类都增加了一块很复杂的代码,同时我们应当认识到,虽然AoDi类和BenChi类的制造细节不同但是大体的流程是差不多的,大家都由引擎,轮胎,方向盘等组成,这些相似的代码在不同的类中重复是很不优雅的。
像这种产品的组成部分相同但是具体生产细节不同的情况特别适合使用创建者模式。
首先我们需要一个公共的汽车制造商抽象类:
// 抽象建造类,提供创建产品的共同接口,不同的产品可以有自己的具体实现
public abstract class AbstractBuilder {
// 创建引擎
public abstract void buildEngine();
// 创建车玻璃
public abstract void buildGlass();
// 创建方向盘
public abstract void buildSteeringWheel();
// 返回创建好的产品,为了兼容所有的产品,返回的类型定为共同的父类
public abstract Car getCar();
}
以及汽车的抽象类和它的各个实现类,比如奥迪汽车、宝马汽车…:
// 抽象公共汽车父类
public abstract class Car {
// 汽车引擎,实际应用中应该是一个对象,这里用字符串来表示
public String engine;
// 汽车玻璃,不同的汽车大小不一样,需要根据汽车的型号计算
public double glass;
// 汽车方向盘
public String steeringWheel;
public abstract void drive();
}
// 奥迪车
class AoDi extends Car {
public void drive() {
System.out.println("开奥迪......");
}
}
然后我们需要实现奥迪汽车制造商、奔驰汽车制造商…(这里以奥迪的制造商为例)
//AoDi的具体创建者
public class AoiBuilder extends AbstractBuilder{
AoDi aodi = new AoDi();
/** * 创建各个部分 */
public void buildEngine() {
aodi.engine = "AoDiEngine";
}
public void buildGlass() {
aodi.glass = 3.5;
}
public void buildSteeringWheel() {
aodi.steeringWheel = "AoDiSteeringWheel";
}
// 返回创建好的对象
public Car getCar() {
return this.aodi;
}
}
再然后,我们来实现奥迪汽车的拼装环节,它由生产商来完成:
// 生产商,它负责将其他制造商的零件进行拼凑
public class Producer {
private AbstractBuilder builder;
public Director(AbstractBuilder builder){
this.builder = builder;
}
// 组成产品的方法,组成的过程可能是有顺序的
public Car construct() {
builder.buildSteeringWheel();
builder.buildGlass();
builder.buildEngine();
return builder.getCar();
}
}
然后我们来模拟客户来买车:
public class Person {
public static void main(String[] args) throws Exception {
// 生产商来啦
Producer producer = new Producer(new AoDiBuilder());
// 给我组辆AE86
Car car = director.construct();
//开车
car.drive();
}
}
到此结束。
模式优点
- 在建造者模式中, 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
- 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
- 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
- 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。
模式缺点
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
使用场景
- 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
- 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
- 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中。
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
建造者模式与工厂模式的区别?
- 工厂模式一般都是创建一个产品,注重的是把这个产品创建出来就行,只要创建出来,不关心这个产品的组成部分。从代码上看,工厂模式就是一个方法,用这个方法就能生产出产品。
- 建造者模式也是创建一个产品,但是不仅要把这个产品创建出来,还要关系这个产品的组成细节、组成过程。从代码上看,建造者模式在建造产品时,这个产品有很多方法,建造者模式会根据这些相同方法但是不同执行顺序建造出不同组成细节的产品。
换言之,工厂模式处在更加具体的尺度上,而建造者模式处在更加宏观的尺度上。一个系统可以由一个建造模式和一个工厂模式组成,客户端通过调用这个创建角色,间接地调用另一个抽象工厂模式的工厂角色。工厂模式返回不同产品族的零件,而建造者模式则把他们组装起来。
最佳示例
StringBuilder
StringBuilder对于大家肯定很熟悉,它就是用了建造者模式。
public StringBuilder append(String str) {
super.append(str);
return this;
}
BeanDefinitionBuilder
Spring 中的 BeanDefinitionBuilder,可以看到它的方法中有很多设置属性、依赖、作用域等等属性的方法。
public final class BeanDefinitionBuilder {
private final AbstractBeanDefinition beanDefinition;
public AbstractBeanDefinition getRawBeanDefinition() {
return this.beanDefinition;
}
...
}
SqlSessionFactoryBuilder
MyBatis中的SqlSessionFactoryBuilder类,也是用了建造者模式,返回SqlSessionFactory对象。
通过重载build()方法,传递默认参数,屏蔽创建细节。
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment) {
return build(reader, environment, null);
}
public SqlSessionFactory build(Reader reader, Properties properties) {
return build(reader, null, properties);
}
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse()); // 最终调用的地方
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
}
}
}
...
}
原型模式
概述
原型模式(Prototype),就是用原型实例指定创建对象的种类并且通过拷贝这些原型对象创建新的对象
。
在Java中提供了clone()方法来实现对象的克隆,所以原型模式实现变得简单的多了。
clone()方法
Java的所有类都是从java.lang.Object
类继承而来的,而Object类提供下面的方法对对象进行复制:
protected native Object clone() throws CloneNotSupportedException;
子类也可以将这个方法覆盖掉,用自己的逻辑实现自己的复制方法。可以被使用clone()
方法的类都必须实现Cloneable
接口,Cloneable
接口只起一个作用就是在运行时期通知Java虚拟机可以安全地在这个类上使用clone()
方法。
克隆又分为两种:浅克隆、深克隆。
浅克隆
浅克隆模型结构如下:
浅度复制只是复制对象的值,我们知道对象的属性一共分为两种,基本类型
和引用类型
。
- 对于浅度复制基本类型的数据会复制一份到新的对象中去,
- 对于引用类型的属性仅仅复制引用的值,引用所指向的具体的对象不会复制,所以A和B实际上是用的同一个对象c,如果再A中改变c的属性,B中也能看到,因为改变的是两者共有对象。
Java提供的clone方法就是这种类型的。
深克隆
深克隆模型结构如下:
深度复制与浅度复制的不同就是深度复制不但会复制对象的引用,并且还会复制引用所指的对象。
所以在第二幅图中A和B是指向的不同的对象,此时在A中操作c对象不会对B产生任何影响。
满足克隆的条件
- 对任何对象x,都有
x.clone() != x
换言之,克隆对象与原来的对象不是同一个对象。 - 对任何对象x,都有
x.clone().getClass == x.getClass()
,换言之,克隆对象与原对象的类型一致。 - 如果对象x的equals()方法定义恰当的话,那么
x.clone().equals(x)
应该是成立的。- 这里的equals()方法定义恰当,是指按照被克隆的对象他们的内部状态是否可变,划分为可变对象和不可变对象(String的内部数值是不能改变的)。
- 对于可变对象只有当他们是同一个对象时才会返回true。
- 而对于不变对象,当他们的内部状态值是一样的时候就认为是true,但是内部状态一直的不一定就是同一个对象。
- 这里的equals()方法定义恰当,是指按照被克隆的对象他们的内部状态是否可变,划分为可变对象和不可变对象(String的内部数值是不能改变的)。
两类原型模式
原型模式一般分为两种,一种是不带管理类的原型模式(简单形式),另一种是带管理类的原型模式(登记形式)。
简单形式和登记形式的原型模式各有其长处和短处,如果需要创建的原型对象数目较少 而且比较固定的话可以采取简单形式,如果创建的原型对象数目不固定的话建议采取第二种形式。
1.不带管理类的原型模式
首先介绍不带管理类的原型模式:
模型结构图
这种形式涉及到三个角色:
- 客户角色:客户提出创建对象的请求。
- 抽象原型角色:这是一个抽象角色,通常由一个Java接口或Java抽象类实现,这个类可能会继承
Cloneable
接口。 - 具体原型角色:被复制的对象。此角色需要实现抽象的原型角色所要求的接口。
示例代码
以抄作业为例,作业分为数学作业
和信息编程作业
等,这里我们来模拟一下:
// 作业 抽象类
public abstract class Homework implements Cloneable {
public abstract Object clone();
public abstract void show();
}
// 数学作业
public class MathHomework extends Homework{
private Object A = new Object(); // 引用类型
private int a = 1; // 基本类型
public void submit() {
System.out.println("上交数学作业...");
}
// 实现自己的克隆方法
public Object clone(){
MathHomework m = null;
// 深克隆
m = (MathHomework) this.clone();
m.A = (Object) this.getA().clone();
return m;
}
public Object getA() {
return A;
}
}
这时候很多同学过来抄作业,代码如下:
public static void main(String[] args) {
// 这是班级里的学霸的数学作业
MathHomework xueba = new MathHomework();
// 各位学渣都来抄作业
MathHomework zhangsan = (MathHomework)xueba.clone();
MathHomework lisi = (MathHomework)xueba.clone();
MathHomework wangwu = (MathHomework)xueba.clone();
zhangsan.submit();
lisi.submit();
wangwu.submit();
}
2.带管理类的原型模式
再来介绍带管理类的原型模式:
模型结构图
示例代码
这时候我们编写一个掌控所有学霸的管理组织:
// 这是一个掌控所有学霸的组织,所有的学霸都要听他的,包括数学学霸-wuyifan、编程学霸-zhangsan、语文学霸-lisi...
public class Manager {
private static volatile Manager manager;
private Map<String, Homework> homeworks;
private Manager() {
prototypes = new ConcurrentHashMap<String, Homework>();
}
//使用了简单工厂模式,单例模式
public static Manager getManager() {
if (manager == null)
synchronized(Manager.class) {
if (manager == null) {
manager = new Manager();
}
}
return manager;
}
// 收学霸的作业
public void put(String name,Homework homework){
homeworks.put(name, homework);
}
// 抄指定人的作业
public Homework getHomework(String name){
if(homeworks.containsKey(name)){
return (Homework) ((Homework)homeworks.get(name)).clone();
}else{
System.out.println("暂时没有" + name + "的作业可以抄");
return null;
}
}
}
这个组织来给大家抄作业了,代码如下:
public static void main(String[] args){
MathHomework wuyifan = new MathHomework();
JavaHomework zhangsan = new JavaHomework();
Manager.getManager().put("数学作业", wuyifan);
Manager.getManager().put("java作业", zhangsan);
// 抄一份新的数学作业
MathHomework newMathHomework = (MathHomework) Manager.getManager().getHomework("数学作业");
JavaHomework newJavaHomework = (JavaHomework) Manager.getManager().getHomework("Java作业");
newMathHomework.show();
newJavaHomework.show();
}
模式优点
- 将产品的创建过程封装起来,客户端不需要了解产品的具体创建流程。
- 利用Java的clone方法来创建对象肯定要比使用new来创建对象快很多,尤其是那些很复杂的对象的时候。
- 可以在不修改其他代码的情况下添加新的产品,符合“开-闭”原则。
模式缺点
每一个类必须都有一个clone方法,如果这个类的组成不太复杂的话还比较好,如果类的组成很复杂的话,如果想实现深度复制就非常困难了。
使用场景
假设一个系统的产品类是动态加载的,而且产品类具有一定的等级结构。这个时候如果采用工厂模式的话,工厂类就不得不具有一个相应的等级结构。而产品类的等级结构一旦发生变化,工厂类的等级结构就不得不有一个相应的变化,这对于产品结构可能经常变化的系统来说采用工厂模式是很不方便的。这个时候如果采用原型模式,给每个产品类装配一个clone方法便可以避免使用工厂方式所带来的具有固定等级结构的工厂类。