创建者模式大汇总
源代码地址
单例模式
单例模式确保了在整个系统中这个类只有一个对象被创建。他提供了一种访问他唯一的一个对象的方式,可以直接访问,不需要额外实例化对象。
单例模式有几个显著的特点:
- 私有化构造方法,这样就不能通过构造方法来创建对象了。
- 对外提供静态方法来让外接获取对象。
- 私有化静态变量,来保证全局只有一个变量。
分类
单例模式分为两类:
- 饿汉式:类加载的时候对象就会被创建
- 懒汉式:类加载时对象不会被创建,首次使用的时候类才会被创建。
饿汉式的几种实现方式
类加载的时候就生成了对象,但是不用的话就很浪费内存.
静态变量方法
public class HungrySingleton1 {
private HungrySingleton1(){}
private static HungrySingleton1 instance=new HungrySingleton1();
public static HungrySingleton1 getInstance(){
return instance;
}
}
静态代码块方法
public class HungrySingleton2 {
private HungrySingleton2(){}
private static HungrySingleton2 instance;
static{
instance=new HungrySingleton2();
}
public static HungrySingleton2 getInstance(){
return instance;
}
}
上述的这两种方式其实差不多,就是在类里面new了一个私有静态变量,然后对外的方法只返回他。
使用枚举类实现单例模式
public enum HungrySingleton3 {
INSTANCE;
public void method1(){
System.out.println("do sth");
}
}
怎么样是不是非常的简单,而且枚举类的实现方式是最安全的。不会被反射或者序列化破解。
懒汉式的几种实现方式
懒汉式在类被使用到的时候,才会生成对象避免了内存的浪费。
线程不安全的懒汉式
这个方式就是在调用
getInstance()
方法的时候会先判断是否已经创建了对象。如果创建了就返回,没有就创建一个在返回。但是在多线程的情况下,有可能出二次创建的现象。
public class Lazy1 {
private Lazy1() {
}
private static Lazy1 instance;
public static Lazy1 getInstance() {
if (instance == null) {
instance=new Lazy1();
}
return instance;
}
}
线程安全的懒汉方
和上面的差异就是多了一个
synchronized
关键字。使用synchronized关键字给
getInstance()
方法加锁,但是锁的加在了整个方法上,但是性能较差。
public class Lazy1 {
private Lazy1() {
}
private static Lazy1 instance;
public static synchronized Lazy1 getInstance() {
if (instance == null) {
instance=new Lazy1();
}
return instance;
}
}
双重检查模式
双重检查锁方式,把锁加在了方法里面而不是整个方法。只有在需要创建对象的时候才需要获得锁,让锁的粒度更细了。效率也就更高。
public class Lazy2 {
private Lazy2(){}
//volatile 是为了保证指令的有序性,不然在多线程环境下 由于jvm的指令重排序可能会导致空指针
private static volatile Lazy2 instance;
public static Lazy2 getInstance(){
if(instance==null){
synchronized(Lazy2.class){
if(instance==null){
instance=new Lazy2();
}
}
}
return instance;
}
}
静态内部类模式
通过在对象里面创建一个静态内部类来持有对象。静态内部类在类加载是不会被加载,只有在被调用的时候才会被加载,从而实现了懒汉式。而且是线程安全的。
public class Lazy3 {
private Lazy3(){}
private static class Lazy3Holder{
private static final Lazy3 INSTANCE=new Lazy3();
}
public static Lazy3 getInstance(){
return Lazy3Holder.INSTANCE;
}
}
问题
序列化、反序列化破坏单例模式
先说一下基本的概念,
- 序列化:把对象转换成字节流,然后写入到文件或者通过网络传输给其他终端。
-
反序列化:把字节流重新转化为对象。
因此我们把类序列化成字节流,然后保存到文件中.就能做到创建对象。
public static void write2File() throws FileNotFoundException, IOException {
Lazy2 l = Lazy2.getInstance();
ObjectOutputStream oStream = new ObjectOutputStream(new FileOutputStream("obj.txt"));
oStream.writeObject(l);
oStream.close();
}
然后通过字节流在反序列化成对象。
public static Lazy2 read4File() throws FileNotFoundException, IOException, ClassNotFoundException{
ObjectInputStream oInputStream=new ObjectInputStream(new FileInputStream("obj.txt"));
Lazy2 l=(Lazy2)oInputStream.readObject();
oInputStream.close();
return l;
}
我们直接反序列化两次,生成两个对象。输出结果为:
false
。说明我们生成了两个不同的对象,破坏了单例模式全局唯一对象的特性。
public static void main(String[] args) throws Exception{
write2File();
System.out.println(read4File()==read4File());
}
反射破解单例模式
反射可以通过一个类对应的class类来获得这个类
所有
的成员变量和方法,
不管是私有的还是公开的
。
可以理解为,每个类都有一个大门。门里面是他的私有的变量和方法,只有类自己有钥匙打开大门去操作他们。但是反射不讲武德,他有电锯。直接把门锯开了,然后去操作这个类私有的变量和方法。
public class Reflection {
public static void main(String[] args) throws Exception{
//获取Lazy2的class类
Class<Lazy2> class1=Lazy2.class;
//获取私有构造方法
Constructor con=class1.getDeclaredConstructor();
//暴力破门(设置私有方法可访问)
con.setAccessible(true);
//使用构造方法创建对象
Lazy2 l=(Lazy2) con.newInstance();
Lazy2 l2=(Lazy2) con.newInstance();
System.out.println(l==l2);
}
}
输出:
false
解决方式
一下的例子我都是在
Lazy2
上修改的
序列化、反序列化
在需要序列化反序列化的单例模式类中添加
readResolve()
方法来规定反序列化时的返回值。
在反序列化时,Java会判断是否有这个函数,如果有就返回这个函数的返回值。没有就会new一个新的对象来返回
public Object readResolve(){
return instance;
}
public static void main(String[] args) throws Exception{
write2File();
Lazy2 l=read4File();
Lazy2 l2=read4File();
System.out.println(l==l2);
}
输出:
true
反射
既然反射时通过调用构造方法来实现的创建对象的,那么我们就在构造方法里面加上一点逻辑判断,在有对象的情况下报错,不让他创建就好了。
private Lazy2(){
synchronized(Lazy2.class){
if(instance!=null){
throw new RuntimeErrorException(null, "不准创建多个对象");
}
}
}
在常规调用之后,在使用反射创建对象就会报错。
Lazy2 l=Lazy2.getInstance();
Lazy2 l1=(Lazy2) con.newInstance();
但是直接使用反射还是可以破解
Lazy2 l1=(Lazy2) con.newInstance();
Lazy2 l2=(Lazy2) con.newInstance();
反射实在太猛了
枚举大法好!
Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的。完美符合单例模式!
简单工厂模式
使用一个接口
A
来规定工厂生产的产品的的规范。然后使用这个接口
A
的实现类来说明实际生产的产品。
举例
我们的程序需要输出日志
public class Log {
public void log(String name){
if (name.equals("txt")) {
System.out.println("往txt中输出数据");
} else if (name.equals("cmd")) {
System.out.println("往CMD中输出数据");
}
}
}
但是一般情况下,我们会有很多种
Log
类,如果每个
Log
类都可以往txt和命令行中输出日志。假设有n种
Log
类,那么这段代码我们就要写n次,这问题还不大。但是突然有一天,你需要往一数据库里面写日志了,那我们就需要修改上面的代码,改成这样。
public class Log {
public void log(String name){
if (name.equals("txt")) {
System.out.println("往txt中输出数据");
} else if (name.equals("cmd")) {
System.out.println("往CMD中输出数据");
} else if(name.equals("db")){
System.out.println("往数据库种输出日志");
}
}
}
改一次还简单,但是别忘了
我们有n个Log类
,这就要了老命了。我们要去n个地方改,而且忘记改了就会导致系统出错。
因此我们可以使用简单工厂模式。
-
抽象出一个
Log
产品
我们定义一个
Log
接口来规范
Log
的功能。
public interface Log{
public void writeLog();
}
-
写实现类
CMDLog
,
TxtLog
来表示具体
Log
的类(具体的产品)
public class CMDLog implements Log{
@Override
public void writeLog() {
System.out.println("往CMD中输出数据");
}
}
public class TxtLog implements Log{
@Override
public void writeLog() {
System.out.println("往txt中输出数据");
}
}
-
定义一个
LogFactory
工厂来返回Log产品
public class LogFactory {
public static Log createLog(String name) {
if (name.equals("txt")) {
return new TxtLog();
} else if (name.equals("cmd")) {
return new CMDLog();
} else {
return null;
}
}
}
-
不同的
Log
调用工厂来获得
Log
实现类,进行业务操作即可
public class ApacheLog {
public void log(String name){
Log log=LogFactory.createLog(name);
log.writeLog();
}
public void ApacheLogMethod(){
System.out.println("Apache日志专属方法");
}
}
public class SysLog {
public void log(String name){
Log log=LogFactory.createLog(name);
log.writeLog();
}
public void sysLogMethod(){
System.out.println("系统日志专属方法");
}
}
SysLog
和
ApacheLog
都有他们独有的方法,但是又可以随意的往任何地方输出日志,虽然最后还是用了if/else来判断具体往哪里输出日志,但是当我们需要增加新的需求时,我们只需要修改
LogFactory
的代码和新增一个实现类就好了。不需要去修改
SysLog
和
ApacheLog
了。
测试
public static void main(String[] args) {
ApacheLog apacheLog=new ApacheLog();
SysLog sysLog=new SysLog();
apacheLog.log("txt");
apacheLog.ApacheLogMethod();
sysLog.log("cmd");
sysLog.sysLogMethod();
}
输出:
往txt中输出数据
Apache日志专属方法
往CMD中输出数据
系统日志专属方法
优点:
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
- 屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:
每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度。
使用场景:
- 日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
- 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
- 设计一个连接服务器的框架,需要三个协议,“POP3”、“IMAP”、“HTTP”,可以把这三个作为产品类,共同实现一个接口。
注意事项
作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
工厂方法模式
相对于简单工厂模式,工厂方法模式完全符合了开闭原则(对修改关闭,对扩展开放)
概念
工厂方法模式定义一个工厂接口来用于创建对象,然后让其子类来决定具体创建哪个对象。工厂方法让一个产品类的创建延迟到了其工厂的子类中。
结构
- 抽象工厂:提供创建产品的接口,通过它来访问具体工厂并创建产品
- 具体工厂:抽象工厂的实现子类,实现了抽象工厂的方法,完成产品的创建
- 抽象产品:定义了产品规范,描述了产品的功能和特性
- 具体产品:抽象产品的实现子类,由具体工厂创建和具体工厂一一对应。
类图
优缺点
优点
:
-
在系统增加新的产品的时候,只需要添加具体的产品类和对应的具体工厂,不用修改原代码。满足了
开闭原则
- 用户只要直到具体工厂的名字就能得到产品,无需关注产品具体的创建过程。
缺点
:
- 每次加新产品就会多两个类,长此以往类会非常多,增加系统的复杂度
抽象工厂模式(AbstractFactory)
概念
上述的简单工厂模式和工厂方法模式都只是生产同一种类的产品。比如咖啡工厂只生产咖啡,牧场只养动物,九阳豆浆机只造豆浆机,手机店只卖手机。但是在现实情况下,店家不会这么单纯滴。就比如华为手机店,就不可能只卖手机,他还买电脑、电视甚至是汽车(对的,华为还卖汽车哦)。
也就是说简单工厂模式和工厂方法模式都只能生产
同一类的产品
(不同的咖啡,或者不同品种的狗狗),而抽象工厂可以
生产同一族,但是不同类的产品
(同时生产手机和电脑,同时生产上衣和裤子)
关于同类和同族,举例来说就是所有电子产品都是同一族的,比如电脑和手机是同一族的。然后一个产品的不同类型是同一类的,比如华为mate20和华为p30。
具体结构
结构其实和工厂方法是一样的,但是有一些细微的区别
- 抽象工厂:包含了多个创建产品的方法,可以创建不同类的产品(工厂方法只能创建一类)
- 具体工厂:抽象工厂的实现类
- 抽象产品:定义了产品的规范(可以认为同一个接口定义的产品就是同一类)
- 具体产品:抽象产品的实现类
类图
优缺点
-
优点:
一个产品族中的产品只需要一个类就行了
。当一个产品族中多个对象(手机和电脑)被设计成一起工作时(一起被摆在华为的店里面卖),他能保证客户端只使用用一个产品族的对象(顾客就在华为店里面只能买到华为的,买不到苹果的)。 - 缺点:当一个产品族需要增加一个新产品时(抽象接口里面多了个新方法,就像华为突然开始卖车),所有的工厂类都需要修改。(所有实现类都得实现这个方法,都得加上生产车的方法)
使用场景
由于抽象工厂在使用的时候会有非常多的类,所以不是什么时候都适合用他的。(比如我这咖啡店就只卖咖啡)
- 当被创建的对象时同一个产品族的(华为店里面的手机、电脑、耳机)
- 系统里面有很多个产品族,但是用户一般情况下使用其中一族(比如一般用户都是用苹果全家桶或者华为全家桶)
原型模式
他使用一个已经被创建的实例作为模板,通过复制来创建一个和原型对象相同的新对象。
结构
-
抽象原型类:规定了具体原型对象必须实现
clone()
方法。(
Cloneable
) - 具体原型类:实现抽象原型类的clone()方法,他是可被复制的对象
- 访问类:使用clone()方法来复制。
类图及实现
原型模式的复制方式有两种:
浅拷贝
:直接复制了对象A地址给另一个对象B。由于对象A,B指向的是同一个地址因此修改B,A也会跟着改变。
深拷贝
:复制了对象A的值给另一个对象B,这两个对象没有任何关系指向不同的地址。修改其中一个并不会对另一个造成影响。
接下来的例子我都按浅拷贝来实现了,
有关浅拷贝和深拷贝的详细内容,看这里
使用场景
- 对象的创建很复杂,就可以直接复制来快捷的获得对象
- 性能和安全的要求比较高
建造者模式
- 建造者模式将一个复杂对象的构建和表示分离,让同样额构建过程可以表示不同的产品。
-
他分离了产品部分的构造和装配。分别有
Builder
和
Director
负责,并最终可以生成复杂的对象。这个模式适用于:生成一个及其复杂的对象的情况。 - 由于构造和装配的解耦,因此相同的构造,不同的装配可以生成不同的对象。不同的构造,相同的装配,不同的构造也可以生成不同的对象。(可以理解为做汉堡。构造就是汉堡的肉是什么肉,菜是什么菜,面包是什么面包。装配就是放的顺序,可以肉夹面包也可以面包夹肉)
- 建造者模式将产品和组装的过程分开。用户只需要指定复杂对象的类型就可以获得该对象,无需直到具体的对象的创建细节
结构
-
抽象建造者(
Builder
):规定建造者需要实现的方法,不涉及具体的对象部件的创建 - 具体建造者:抽象建造者的实现类,实现了其中的方法,负责具体对象部件的创建
- 产品:要被创建的对象
-
指挥者(
Director
):调用具体建造者来创建复杂对象的各个部件,并按照顺序把他们拼起来。
案例类图
优缺点
优点:
-
建造者模式的封装性很好,可以有效的应对产品部件的变化,而且业务逻辑集中在
Director
中稳定性较好。 - 客户端不需要知道产品的创建细节,将产品本身和产品的创建过程解耦,使相同的创建过程可以创建不同的产品对象。
- 可以精细的控制产品的创建过程。
-
符合开闭原则,很容易扩展。想要有个新的产品只要多搞一个
Builder
的实现类也就是具体建造者就行了。
缺点: - 产品更新迭代需要加一个新部件的话,就需要修改一大堆具体建造者类。
- 如果产品直接的差异性过大就不能用建造者模式
使用场景
建造者模式通常用来创建复杂对象。这种对象的部件变化频繁,但是组合的过程相对稳定。
- 对象复杂,由多个部件组成。但是部件之间的建造顺序稳定
- 产品的构建过程和最终的表示是独立的
相似创建者模式的对比
工厂方法模式和建造者模式
工厂方法模式
注重整体对象的创建,而
建造者模式
注重部件构造的过程,通过一个个部件的搭建来最终生成一个复杂对象。
也就是说
工厂方法模式
生成电脑就是直接生成一台电脑,
建造者模式
就是先拿到cpu、gpu然后主板什么的最终拼成一台电脑。
抽象工厂模式和建造者模式
抽象工厂模式
关注的是一个产品族的生产。他不关心构建的过程,只关心什么产品由什么工厂生产即可。
建造者
则是按照指定的蓝图构建产品,他的目的是通过组装零件来生产一个新产品。
如果
抽象工厂模式
是汽车配件生产厂,那
建造者
就是汽车组装厂。由抽象工厂生产零件,再由建造者组装。