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

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

创建型模式分为:

  • 单例模式
  • 工厂方法模式
  • 抽象工程模式
  • 原型模式
  • 建造者模式

单例模式

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

即在整个程序中,同一个类始终只会有一个对象来进行操作。比如数据库连接类,实际上只需要创建一个对象或是直接使用静态方法就可以了,没必要去创建多个对象。

单例设计模式分类两种:

  • 饿汉式:类加载就会导致该单实例对象被创建
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

饿汉式

/**
* 饿汉式
* 静态变量创建类的对象
*/
public class Singleton {
//私有构造方法
private Singleton() {}

//在成员位置创建该类的对象
private static Singleton instance = new Singleton();

//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}

该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

  • 懒汉式-方式1
/**
* 懒汉式1
*/
public class Singleton {
//私有构造方法
private Singleton() {}

//在成员位置创建该类的对象
private static Singleton instance;

//对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}

该方式在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

  • 懒汉式-方式2(双重检查锁)

再来讨论一下懒汉模式中加锁的问题,对于getInstance()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式

/**
* 双重检查方式
*/
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;
}
}

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作

要解决双重检查锁模式带来空指针异常的问题,只需要使用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;
}
}

添加volatile关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题

  • 懒汉式-方式3(静态内部类方式)

静态内部类单例模式中实例由内部类创建,由于 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;
}
}

第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

工厂模式

在java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的。所以说,工厂模式最大的优点就是:解耦

简单工厂模式

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

设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能

代码如下:

public abstract class Coffee {
public abstract String getName(); // 获取咖啡名称
public abstract void prepare(); // 准备咖啡
}

public class AmericanCoffee extends Coffee {
@Override
public String getName() {
return "美式咖啡";
}

@Override
public void prepare() {
System.out.println("准备美式咖啡:加水,加咖啡豆");
}
}

public class LatteCoffee extends Coffee {
@Override
public String getName() {
return "拿铁咖啡";
}

@Override
public void prepare() {
System.out.println("准备拿铁咖啡:加牛奶,加咖啡豆");
}
}

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 class CoffeeStore {
public Coffee orderCoffee(String type) {
// 通过工厂创建咖啡对象
Coffee coffee = CoffeeFactory.createCoffee(type);
// 准备咖啡
coffee.prepare();
// 返回咖啡
return coffee;
}
}

工厂(factory)处理创建对象的细节,一旦有了SimpleCoffeeFactoryCoffeeStore类中的orderCoffee()就变成此对象的客户,后期如果需要Coffee对象直接从工厂中获取即可。这样也就解除了和Coffee实现类的耦合,同时又产生了新的耦合,CoffeeStore对象和SimpleCoffeeFactory工厂对象的耦合,工厂对象和商品对象的耦合。

后期如果再加新品种的咖啡,我们势必要需求修改SimpleCoffeeFactory的代码,违反了开闭原则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修改操作。

工厂方法模式

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

利用对扩展开放,对修改关闭的性质,定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。改进:

public abstract class Coffee {
public abstract String getName(); // 获取咖啡名称
public abstract void prepare(); // 准备咖啡
}

public class AmericanCoffee extends Coffee {
@Override
public String getName() {
return "美式咖啡";
}

@Override
public void prepare() {
System.out.println("准备美式咖啡:加水,加咖啡豆");
}
}

public class LatteCoffee extends Coffee {
@Override
public String getName() {
return "拿铁咖啡";
}

@Override
public void prepare() {
System.out.println("准备拿铁咖啡:加牛奶,加咖啡豆");
}
}

public interface CoffeeFactory {
Coffee createCoffee(); // 创建咖啡
}

public class AmericanCoffeeFactory implements CoffeeFactory {
@Override
public Coffee createCoffee() {
return new AmericanCoffee();
}
}

public class LatteCoffeeFactory implements CoffeeFactory {
@Override
public Coffee createCoffee() {
return new LatteCoffee();
}
}

public class CoffeeStore {
private CoffeeFactory coffeeFactory;

public CoffeeStore(CoffeeFactory coffeeFactory) {
this.coffeeFactory = coffeeFactory;
}

public Coffee orderCoffee() {
// 通过工厂创建咖啡对象
Coffee coffee = coffeeFactory.createCoffee();
// 准备咖啡
coffee.prepare();
// 返回咖啡
return coffee;
}
}

这样用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程,并且在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则。

但是这种方法每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。

抽象工厂模式

前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机等。

这些工厂只生产同种类产品,同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。

抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示横轴是产品等级,也就是同一类产品;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂。

project

假设上面案例中,咖啡工厂不止生成咖啡,还生产甜点,我们可以用抽象工厂模式扩展:

// 抽象工厂类
public interface CafeFactory {
Coffee createCoffee(); // 创建咖啡
Dessert createDessert(); // 创建甜点
}

// 具体工厂类
public class AmericanCafeFactory implements CafeFactory {
@Override
public Coffee createCoffee() {
return new AmericanCoffee();
}

@Override
public Dessert createDessert() {
return new Cake();
}
}

public class LatteCafeFactory implements CafeFactory {
@Override
public Coffee createCoffee() {
return new LatteCoffee();
}

@Override
public Dessert createDessert() {
return new Pudding();
}
}

// Stone类
public class CoffeeStore {
private CafeFactory cafeFactory;

public CoffeeStore(CafeFactory cafeFactory) {
this.cafeFactory = cafeFactory;
}

public Coffee orderCoffee() {
Coffee coffee = cafeFactory.createCoffee();
coffee.prepare();
return coffee;
}

public Dessert orderDessert() {
Dessert dessert = cafeFactory.createDessert();
dessert.serve();
return dessert;
}
}

如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。这样当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

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

原型模式

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象

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

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

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

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

class Address {
String city;

Address(String city) {
this.city = city;
}
}

class Person implements Cloneable {
String name;
Address address;

Person(String name, Address address) {
this.name = name;
this.address = address;
}

// 浅克隆
@Override
public Person clone() {
try {
return (Person) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}

public class Main {
public static void main(String[] args) {
Address address = new Address("Beijing");
Person person1 = new Person("Alice", address);
Person person2 = person1.clone();

System.out.println(person1.address.city); // 输出: Beijing
System.out.println(person2.address.city); // 输出: Beijing

person2.address.city = "Shanghai"; // 修改 person2 的地址
System.out.println(person1.address.city); // 输出: Shanghai(person1 也受到影响)
}
}

虽然这两者不是同一对象,但是这两者指向的对象是同一个,修改任意一方的数据,对另一方也生效。

深克隆就需要把克隆对象的内部字段也克隆一遍:

class Address implements Cloneable {
String city;

Address(String city) {
this.city = city;
}

// 深克隆
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}

class Person implements Cloneable {
String name;
Address address;

Person(String name, Address address) {
this.name = name;
this.address = address;
}

// 深克隆
@Override
public Person clone() {
try {
Person cloned = (Person) super.clone();
cloned.address = this.address.clone(); // 递归克隆引用类型字段
return cloned;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}

public class Main {
public static void main(String[] args) {
Address address = new Address("Beijing");
Person person1 = new Person("Alice", address);
Person person2 = person1.clone();

System.out.println(person1.address.city); // 输出: Beijing
System.out.println(person2.address.city); // 输出: Beijing

person2.address.city = "Shanghai"; // 修改 person2 的地址
System.out.println(person1.address.city); // 输出: Beijing(person1 不受影响)
}
}

建造者模式

建造者模式将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示,适用于某个对象的构建过程复杂的情况

  • 分离了部件的构造(由Builder来负责)和装配(由Director负责)。 从而可以构造出复杂的对象
  • 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦,实现了更好的复用
  • 可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节

举例,假如有一个学生类:

public class Student {
int id;
int age;
int grade;
String name;
String college;
String profession;
List<String> awards;

public Student(int id, int age, int grade, String name, String college, String profession, List<String> awards) {
this.id = id;
this.age = age;
this.grade = grade;
this.name = name;
this.college = college;
this.profession = profession;
this.awards = awards;
}
}

如果我们通过new去创建:

public static void main(String[] args) {
Student student = new Student(1, 20, 3, "小明", "计算机学院", "计算机科学与技术", Arrays.asList("A奖", "B奖"));
}

可以看到,光是填参数就非常麻烦,这种情况下就可以使用建造者模式:

public class Student {
// 使用建造者来创建,不对外直接开放
private Student(int id, int age, int grade, String name, String college, String profession, List<String> awards) {
// ...
}

public static StudentBuilder builder(){ //通过builder方法直接获取建造者
return new StudentBuilder();
}

public static class StudentBuilder{ // 创建内部类
// Builder也需要将所有的参数都进行暂时保存,所以Student怎么定义的这里就怎么定义
int id;
int age;
int grade;
String name;
String college;
String profession;
List<String> awards;

public StudentBuilder id(int id){ //直接调用建造者对应的方法,为对应的属性赋值
this.id = id;
return this; //为了支持链式调用,这里直接返回建造者本身,下同
}

public StudentBuilder age(int age){
this.age = age;
return this;
}

...

public StudentBuilder awards(String... awards){
this.awards = Arrays.asList(awards);
return this;
}

public Student build(){ //最后我们只需要调用建造者提供的build方法即可根据我们的配置返回一个对象
return new Student(id, age, grade, name, college, profession, awards);
}
}
}

现在,我们就可以使用建造者来为我们生成对象了:

public static void main(String[] args) {
Student student = Student.builder() //获取建造者
.id(1) //逐步配置各个参数
.age(18)
.grade(3)
.name("小明")
.awards("A", "B", "C")
.build(); //最后直接建造我们想要的对象
}

这样,我们就可以让这些参数对号入座了,并且也比之前的方式优雅许多。