设计模式——创建者模式


创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。

这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

单例模式

单例模式属于创建型模式,这种模式涉及到一个单一的类,该类负责创建自己的对象,保证类在任何情况下都只有⼀个实例,对外提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

内存中只有⼀个实例,减少了对象频繁创建和销毁实例的开销

饿汉式

静态变量方式

说明:instance对象是随着类的加载而创建的。

缺点:如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

/**
* 饿汉式
* 静态变量创建类的对象
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance = new Singleton();
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}

静态代码块方式

说明:和上述方式差不多,随着类的加载而创建

缺点:也存在内存浪费问题

/**
* 恶汉式
* 在静态代码块中创建该类对象
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
static {
instance = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}

懒汉式

线程不安全方式

说明:当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果

缺点:但是,如果是多线程环境,会出现线程安全问题

/**
* 懒汉式
* 线程不安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}

线程安全(synchronized)——效率低

说明:获取实例的方法使用synchronized来解决线程安全

缺点:每个线程必须持有锁才能调用方法,执行效果特别低,而其实只需要在初始化时注意线程安全即可

/**
* 懒汉式
* 线程安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}

线程安全(双重检验锁)——效率高——概率报错空指针

说明:调整加锁时机,绝大部分的操作都是读操作,读操作是线程安全的(解决了单例、性能、线程安全问题

缺点:在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作

/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {//双重检查锁
//抢到锁之后再次判断是否为null
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

空指针错误:

正常执行顺序:

  1. 在堆内存开辟内存空间
  2. 调⽤构造⽅法,初始化对象
  3. 引⽤变量指向堆内存空间

JVM指令重排:

创建对象的执⾏顺序可能为 1 2 3 或者 1 3 2 ,因此当某个线程在乱序运⾏ 1 3 2 指令的时候,引⽤变量指向堆内存 空间,这个对象不为 null,但是没有初始化,其他线程有可能这个时候进 ⼊了 getInstance 的第⼀个 if(instance == null) 判断不为 null ,导致错误使 ⽤了没有初始化的⾮ null 实例,这样的话就会出现异常,这个就是著名的 DCL 失效问题。

线程安全(双重检验锁、volatile)——效率高——反射破坏

说明:使用 volatile 关键字, volatile 关键字可以保证多线程之间变量可见性和有序性

缺点:使用反射可以破坏单例模式

/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

静态内部类方式——无加锁、效率高——序列化破坏

说明:实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性,静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序

在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费

缺点:序列化反序列化能破坏单例模式

/**
* 静态内部类方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

枚举方式

说明:枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次

缺点:无

/**
* 枚举方式
*/
public enum Singleton {
INSTANCE;
}

存在问题

静态内部类方式——序列化反序列化破坏

序列化反序列化可以破坏单例模式

Singleton类:

public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

Test类:

public class Test {
public static void main(String[] args) throws Exception {
//往文件中写对象
//writeObject2File();
//从文件中读取对象
Singleton s1 = readObjectFromFile();
Singleton s2 = readObjectFromFile();
//判断两个反序列化后的对象是否是同一个对象
System.out.println(s1 == s2);
}
private static Singleton readObjectFromFile() throws Exception {
//创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\a.txt"));
//第一个读取Singleton对象
Singleton instance = (Singleton) ois.readObject();
return instance;
}
public static void writeObject2File() throws Exception {
//获取Singleton类的对象
Singleton instance = Singleton.getInstance();
//创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\a.txt"));
//将instance对象写出到文件中
oos.writeObject(instance);
}
}

上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式。

解决办法:readResolve()方法

在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

Singleton类:

public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}

源码解析:

public final Object readObject() throws IOException, ClassNotFoundException{
...
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);//重点查看readObject0方法
.....
}
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
//isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
Object rep = desc.invokeReadResolve(obj);
...
}
return obj;
}

线程安全(双重检验锁、volatile)——反射破坏

使用反射可以破坏单例模式

Singleton类:

public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}

Test类:

public class Test {
public static void main(String[] args) throws Exception {
//获取Singleton类的字节码对象
Class clazz = Singleton.class;
//获取Singleton类的私有无参构造方法对象
Constructor constructor = clazz.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
//创建Singleton类的对象s1
Singleton s1 = (Singleton) constructor.newInstance();
//创建Singleton类的对象s2
Singleton s2 = (Singleton) constructor.newInstance();
//判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(s1 == s2);
}
}

上面代码运行结果是false,表明反射已经破坏了单例设计模式

解决办法:抛出异常

当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。

public class Singleton {
//私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}

JDK源码解析-Runtime类

Runtime类就是使用的单例设计模式。

  1. 通过源代码查看使用的是哪儿种单例模式

    public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    /**
    * Returns the runtime object associated with the current Java application.
    * Most of the methods of class <code>Runtime</code> are instance
    * methods and must be invoked with respect to the current runtime object.
    *
    * @return the <code>Runtime</code> object associated with the current
    * Java application.
    */
    public static Runtime getRuntime() {
    return currentRuntime;
    }
    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    ...
    }

    从上面源代码中可以看出Runtime类使用的是饿汉式(静态属性)方式来实现单例模式的。

  2. 使用Runtime类中的方法

    public class RuntimeDemo {
    public static void main(String[] args) throws IOException {
    //获取Runtime类对象
    Runtime runtime = Runtime.getRuntime();
    //返回 Java 虚拟机中的内存总量。
    System.out.println(runtime.totalMemory());
    //返回 Java 虚拟机试图使用的最大内存量。
    System.out.println(runtime.maxMemory());
    //创建一个新的进程执行指定的字符串命令,返回进程对象
    Process process = runtime.exec("ipconfig");
    //获取命令执行后的结果,通过输入流获取
    InputStream inputStream = process.getInputStream();
    byte[] arr = new byte[1024 * 1024* 100];
    int b = inputStream.read(arr);
    System.out.println(new String(arr,0,b,"gbk"));
    }
    }

工厂模式

一般创建对象时都是直接new一个类的对象,但是如果需要更换为别的类对象时,所有new对象的地方都要修改,与对象耦合度很高,这显然违背了软件设计的开闭原则。此时,我们如果把new对象的活交给工厂来干,然后工厂再提供方法让我们获取该对象,这样需要该对象时就通过该工厂提供的方法获取即可,后面如果想改变获取到的对象,修改该方法就行,也不用去修改获取对象的地方,达到了与对象解耦的目的。所以,工厂模式最大优点就是:解耦

需求:设计一个咖啡店点餐系统

在这里插入图片描述

简单工厂模式

简单工厂不是一种设计模式,反而比较像是一种编程习惯。

在这里插入图片描述

SimpleCoffeeFactory就是对象工厂

代码如下
public class SimpleCoffeeFactory {
public Coffee createCoffee(String type) {
Coffee coffee = null;
if("americano".equals(type)) {
coffee = new AmericanoCoffee();
} else if("latte".equals(type)) {
coffee = new LatteCoffee();
}
return coffee;
}
}
优缺点

优点:

封装了创建对象的过程,客户代码直接通过调用工厂方法并传递不同参数来获取不同对象,把对象的创建操作和业务逻辑层分开,这样就避免了修改对象时需要同时修改客户代码的情况,如果要实现修改获取的对象只需修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。

缺点:

修改产品或新增产品时还是需要修改工厂类的代码,违背了“开闭原则”。

工厂方法模式

针对上例中的缺点,使用工厂方法模式就可以完美的解决,完全遵循开闭原则。

在这里插入图片描述

代码如下

抽象工厂:

public interface CoffeeFactory {
Coffee createCoffee();
}

具体工厂:

public class LatteCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new LatteCoffee();
}
}
public class AmericanCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
}

咖啡店类:

public class CoffeeStore {
private CoffeeFactory factory;
public CoffeeStore(CoffeeFactory factory) {
this.factory = factory;
}
public Coffee orderCoffee(String type) {
Coffee coffee = factory.createCoffee();
coffee.addMilk();
coffee.addsugar();
return coffee;
}
}
优缺点

优点:

客户代码直接使用抽象工厂获取对象即可,通过多态来获取不同对象,如果工厂方法如果要增加新产品,只需要创建新的抽象工厂实现类即可,而无需修改原先的抽象工厂代码,满足了开闭原则

缺点:

每增加一个产品就要新建一个实现类,就会增加系统的复杂度

抽象工厂模式

上述模式的抽象工厂只针对一种产品,即咖啡产品,但是如果增加更多的种类,如甜点,就得新建甜点抽象工厂,再根据不同甜点实现具体工厂,再增加别的,又得再新建工厂,这样很容易发生类爆炸情况,导致出现太多类,这样我们就可以使用抽象工厂解决。

举个例子:拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品族(也就是都属于美式风味),这样我们就可以创建一个风味抽象工厂,该工厂提供咖啡和甜点接口,然后再根据不同风味创建具体的风味工厂,如下:

aaaa

代码如下

抽象工厂:

public interface DessertFactory {
Coffee createCoffee();
Dessert createDessert();
}

具体工厂:

//美式甜点工厂
public class AmericanDessertFactory implements DessertFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
public Dessert createDessert() {
return new MatchaMousse();
}
}
//意大利风味甜点工厂
public class ItalyDessertFactory implements DessertFactory {
public Coffee createCoffee() {
return new LatteCoffee();
}
public Dessert createDessert() {
return new Tiramisu();
}
}

如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。

优缺点

优点:

当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

缺点:

当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。

使用场景
  • 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
  • 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
  • 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。

如:输入法换皮肤,一整套一起换。生成不同操作系统的程序。

模式扩展

简单工厂+配置文件解除耦合

可以通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。

第一步:定义配置文件

为了演示方便,我们使用properties文件作为配置文件,名称为bean.properties

american=com.itheima.pattern.factory.config_factory.AmericanCoffee
latte=com.itheima.pattern.factory.config_factory.LatteCoffee

第二步:改进工厂类

public class CoffeeFactory {
private static Map<String,Coffee> map = new HashMap();
static {
Properties p = new Properties();
InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties");
try {
p.load(is);
//遍历Properties集合对象
Set<Object> keys = p.keySet();
for (Object key : keys) {
//根据键获取值(全类名)
String className = p.getProperty((String) key);
//获取字节码对象
Class clazz = Class.forName(className);
Coffee obj = (Coffee) clazz.newInstance();
map.put((String)key,obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Coffee createCoffee(String name) {
return map.get(name);
}
}

静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象),而读取配置文件以及创建对象写在静态代码块中,目的就是只需要执行一次。

JDK源码解析-Collection.iterator方法

public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("令狐冲");
list.add("风清扬");
list.add("任我行");
//获取迭代器对象
Iterator<String> it = list.iterator();
//使用迭代器遍历
while(it.hasNext()) {
String ele = it.next();
System.out.println(ele);
}
}
}

对上面的代码大家应该很熟,使用迭代器遍历集合,获取集合中的元素。而单列集合获取迭代器的方法就使用到了工厂方法模式。我们看通过类图看看结构:

xxxx

Collection接口是抽象工厂类,ArrayList是具体的工厂类;Iterator接口是抽象商品类,ArrayList类中的Iter内部类是具体的商品类。在具体的工厂类中iterator()方法创建具体的商品类的对象。

另:

  1. DateForamt类中的getInstance()方法使用的是工厂模式;
  2. Calendar类中的getInstance()方法使用的是工厂模式,Calendar 抽象类的 getInstance ⽅法,该方法内部调⽤ createCalendar ⽅法根据不同的地区参数创建不同的⽇历对象
  3. Spring 中的 BeanFactory 使⽤简单⼯⼚模式,根据传⼊⼀个唯⼀的标识来获得 Bean 对象

原型模式

1. 概述

用一个已经创建的实例作为原型,通过Cloneable接口的clone()方法复制该原型对象来创建一个和原型对象相同的新对象。

原型模式的克隆分为浅克隆和深克隆。

浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性(引用类型),仍指向原有属性所指向的对象的内存地址。

深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

Java中的Object类中提供了clone() 方法来实现浅克隆。 Cloneable 接口是上面的类图中的抽象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。

2. 案例1:克隆新对象

接口类图如下:

在这里插入图片描述

Realizetype(具体的原型类):

public class Realizetype implements Cloneable {
public Realizetype() {
System.out.println("具体的原型对象创建完成!");
}
@Override
protected Realizetype clone() throws CloneNotSupportedException {
System.out.println("具体原型复制成功!");
return (Realizetype) super.clone();
}
}

PrototypeTest(测试访问类):

public class PrototypeTest {
public static void main(String[] args) throws CloneNotSupportedException {
Realizetype r1 = new Realizetype();
Realizetype r2 = r1.clone();
System.out.println("对象r1和r2是同一个对象?" + (r1 == r2));//克隆新对象,结果为false
}
}

3. 案例2:修改对象属性

用原型模式生成“三好学生”奖状

同一学校的“三好学生”奖状除了获奖人姓名不同,其他都相同,可以使用原型模式复制多个“三好学生”奖状出来,然后在修改奖状上的名字即可。

类图如下:

在这里插入图片描述

代码如下:

//奖状类
public class Citation implements Cloneable {
private String name;//基本类型会被克隆
public void setName(String name) {
this.name = name;
}
public String getName() {
return (this.name);
}
public void show() {
System.out.println(name + "同学:在2020学年第一学期中表现优秀,被评为三好学生。特发此状!");
}
@Override
public Citation clone() throws CloneNotSupportedException {
return (Citation) super.clone();
}
}
//测试访问类
public class CitationTest {
public static void main(String[] args) throws CloneNotSupportedException {
Citation c1 = new Citation();
c1.setName("张三");
//复制奖状
Citation c2 = c1.clone();
//将奖状的名字修改李四
c2.setName("李四");
c1.show();
c2.show();
}
}

这个Java程序的输出结果将会是:

张三同学:在2020学年第一学期中表现优秀,被评为三好学生。特发此状!

李四同学:在2020学年第一学期中表现优秀,被评为三好学生。特发此状!

分析:虽然对于引用类型说的是浅克隆,也就是仅复制引用而不复制对象。但是因为String类型在Java中是不可变的,一旦String对象被创建,其内容就不能修改,所以在这个程序中,两个Citation对象的name字段实际上是被单独存储在两个不同的String对象中的。当我们调用c2.setName(“李四”)时,实际上是创建了一个新的String对象,包含内容"李四",并把c2的name字段的引用改为指向这个新的String对象。这就是为什么在修改了c2的name字段后,c1的name字段仍然保持不变。

4. 使用场景

  • 对象的创建非常复杂,可以使用原型模式快捷的创建对象。
  • 性能和安全要求比较高。

5. 扩展(深克隆)

将上面的“三好学生”奖状的案例中Citation类的name属性修改为Student类型的属性。代码如下:

//奖状类
public class Citation implements Cloneable {
private Student stu;
public Student getStu() {
return stu;
}
public void setStu(Student stu) {
this.stu = stu;
}
void show() {
System.out.println(stu.getName() + "同学:在2020学年第一学期中表现优秀,被评为三好学生。特发此状!");
}
@Override
public Citation clone() throws CloneNotSupportedException {
return (Citation) super.clone();
}
}
//学生类
public class Student {
private String name;
private String address;
public Student(String name, String address) {
this.name = name;
this.address = address;
}
public Student() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
//测试类
public class CitationTest {
public static void main(String[] args) throws CloneNotSupportedException {
Citation c1 = new Citation();
Student stu = new Student("张三", "西安");
c1.setStu(stu);
//复制奖状
Citation c2 = c1.clone();
//获取c2奖状所属学生对象
Student stu1 = c2.getStu();
stu1.setName("李四");
//判断stu对象和stu1对象是否是同一个对象
System.out.println("stu和stu1是同一个对象?" + (stu == stu1));
c1.show();
c2.show();
}
}

运行结果为:

在这里插入图片描述

说明:

tu对象和stu1对象是同一个对象,就会产生将stu1对象中name属性值改为“李四”,两个Citation(奖状)对象中显示的都是李四。这就是浅克隆的效果,对具体原型类(Citation)中的引用类型的属性进行引用的复制。这种情况需要使用深克隆,而进行深克隆需要使用对象流。代码如下:

public class CitationTest1 {
public static void main(String[] args) throws Exception {
Citation c1 = new Citation();
Student stu = new Student("张三", "西安");
c1.setStu(stu);
//创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\b.txt"));
//将c1对象写出到文件中
oos.writeObject(c1);
oos.close();
//创建对象出入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\b.txt"));
//读取对象
Citation c2 = (Citation) ois.readObject();
//获取c2奖状所属学生对象
Student stu1 = c2.getStu();
stu1.setName("李四");
//判断stu对象和stu1对象是否是同一个对象
System.out.println("stu和stu1是同一个对象?" + (stu == stu1));
c1.show();
c2.show();
}
}

运行结果为:

在这里插入图片描述

注意:Citation类和Student类必须实现Serializable接口,否则会抛NotSerializableException异常。

为什么要用clone()而不用new新建对象

使用clone()方法和new关键字创建对象,在某些情况下,都可以得到相似的结果。但是在以下几个方面,它们具有不同的作用和用途:

  1. 性能:调用clone()方法相比于new创建对象在性能上通常更优,特别是需要重复创建相似对象的场合。clone()方法是基于内存块复制,直接拷贝内存中的二进制流,避免了构造函数和初始化代码的开销。
  2. 对象状态:使用clone()可以得到一个当前对象状态的拷贝,而新建对象需要重新设定初始状态。如果对象的创建过程包括一些复杂的过程和计算,如读取配置文件、数据库操作等,那么clone方法会更有效。
  3. 灵活性:在某些设计模式中(如原型模式),你可能需要使用clone()方法来灵活地创建和复制对象。在不知道具体类型的情况下,可以利用clone()方法生成动态类型的对象。

同时也需要注意,clone()有其自身的问题,如深浅拷贝问题、CloneNotSupportedException异常处理等,使用时需要谨慎。

建造者模式

1.概念

建造者模式(Builder Pattern)是一种对象创建设计模式,它可以让你在不直接创建对象的前提下构建复杂的对象。这种模式主要用于创建一些复杂对象,这些对象的创建过程需要多个步骤,且创建的对象类型相同,但属性的组合有所区别。

建造者模式主要包含四种角色:

  1. Product(产品角色) :一个具体的产品对象。
  2. Builder(抽象建造者) :创建一个Product对象的各个部件指定的接口/抽象类。
  3. ConcreteBuilder(具体建造者) :实现接口,构建和装配各个部件。
  4. Director(指挥者) :构建一个使用Builder接口的对象,它指导建造过程中对象的组装。它并不与产品类发生依赖关系。

类图如下:

在这里插入图片描述

2.案例

创建共享单车

生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有碳纤维,铝合金等材质的,车座有橡胶,真皮等材质。对于自行车的生产就可以使用建造者模式。

这里Bike是产品,包含车架,车座等组件;Builder是抽象建造者,MobikeBuilder和OfoBuilder是具体的建造者;Director是指挥者。类图如下:

在这里插入图片描述

具体的代码如下:

//自行车类
public class Bike {
private String frame;
private String seat;
public String getFrame() {
return frame;
}
public void setFrame(String frame) {
this.frame = frame;
}
public String getSeat() {
return seat;
}
public void setSeat(String seat) {
this.seat = seat;
}
}
// 抽象 builder 类
public abstract class Builder {
protected Bike mBike = new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
}
//摩拜单车Builder类
public class MobikeBuilder extends Builder {
@Override
public void buildFrame() {
mBike.setFrame("铝合金车架");
}
@Override
public void buildSeat() {
mBike.setSeat("真皮车座");
}
@Override
public Bike createBike() {
return mBike;
}
}
//ofo单车Builder类
public class OfoBuilder extends Builder {
@Override
public void buildFrame() {
mBike.setFrame("碳纤维车架");
}
@Override
public void buildSeat() {
mBike.setSeat("橡胶车座");
}
@Override
public Bike createBike() {
return mBike;
}
}
//指挥者类
public class Director {
private Builder mBuilder;
public Director(Builder builder) {
mBuilder = builder;
}
public Bike construct() {
mBuilder.buildFrame();
mBuilder.buildSeat();
return mBuilder.createBike();
}
}
//测试类
public class Client {
public static void main(String[] args) {
showBike(new OfoBuilder());
showBike(new MobikeBuilder());
}
private static void showBike(Builder builder) {
Director director = new Director(builder);
Bike bike = director.construct();
System.out.println(bike.getFrame());
System.out.println(bike.getSeat());
}
}

注意:

上面示例是 Builder模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合

// 抽象 builder 类
public abstract class Builder {
protected Bike mBike = new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
public Bike construct() {
this.buildFrame();
this.BuildSeat();
return this.createBike();
}
}

说明:

这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果construct() 过于复杂,建议还是封装到 Director 中

3.优缺点

优点:

  • 指挥者类建造类过程是稳定者,我们只通过抽象建造者实现不同的具体建造者就可以使得相同的创建过程可以创建不同的产品对象, 将产品本身与产品的创建过程解耦
  • 将复杂的类创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用指挥者来控制创建过程
  • 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。

缺点:

造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

4.使用场景

  • 创建的对象较复杂,由多个部分构成,各部分可能根据不同情况有着复杂的变化(具体建造者不同),但每个部分的创建顺序是稳定的不变的(指挥者不变)。

5.模式扩展:快速创建类(实用)

建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构。

重构前代码如下:

public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;
public Phone(String cpu, String screen, String memory, String mainboard) { //参数过多
this.cpu = cpu;
this.screen = screen;
this.memory = memory;
this.mainboard = mainboard;
}
public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
public String getScreen() {
return screen;
}
public void setScreen(String screen) {
this.screen = screen;
}
public String getMemory() {
return memory;
}
public void setMemory(String memory) {
this.memory = memory;
}
public String getMainboard() {
return mainboard;
}
public void setMainboard(String mainboard) {
this.mainboard = mainboard;
}
@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}
public class Client {
public static void main(String[] args) {
//构建Phone对象
Phone phone = new Phone("intel","三星屏幕","金士顿","华硕");
System.out.println(phone);
}
}

上面在客户端代码中构建Phone对象,传递了四个参数,如果参数更多,代码的可读性会更差,使用就会更复杂。

重构后代码:

public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;
private Phone(Builder builder) {
cpu = builder.cpu;
screen = builder.screen;
memory = builder.memory;
mainboard = builder.mainboard;
}
public static final class Builder {
private String cpu;
private String screen;
private String memory;
private String mainboard;
public Builder() {}
public Builder cpu(String val) {
cpu = val;
return this;
}
public Builder screen(String val) {
screen = val;
return this;
}
public Builder memory(String val) {
memory = val;
return this;
}
public Builder mainboard(String val) {
mainboard = val;
return this;
}
public Phone build() {
return new Phone(this);}
}
@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}
public class Client {
public static void main(String[] args) {
Phone phone = new Phone.Builder()
.cpu("intel")
.mainboard("华硕")
.memory("金士顿")
.screen("三星")
.build();
System.out.println(phone);
}
}

重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。从软件设计上,对程序员的要求比较高。

6.创建者模式对比工厂模式

4.6.1 工厂方法模式VS建造者模式

工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。

我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。

4.6.2 抽象工厂模式VS建造者模式

抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。

建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。

如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。

posted @   Acegzx  阅读(18)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
点击右上角即可分享
微信分享提示