文章目录
在使用设计模式前,我们先用常见的方法来实现封装水果类,以及水果的继承类苹果和橘子
- 定义一个水果抽象类
public abstract class Fruit { // 水果抽象类
private final String name;
public Fruit(String name){
this.name = name;
}
@Override
public String toString() {
return name + "@" + hashCode(); // 打印当前水果名称和 哈希值
}
}
- 定义实现的子类
// 苹果
class Apple extends Fruit{
public Apple(){
super("苹果");
}
}
// 橘子
class Origin extends Fruit{
public Origin(){
super("橘子");
}
}
- 运行测试
public class Demo {
public static void main(String[] args) {
System.out.println(new Apple());
System.out.println(new Origin());
}
}
// 输出结果
// 苹果@685325104
// 橘子@460141958
接下来我们通过一些设计模式来改写上面的例子,以便体会不同设计模式的特点。
一、工厂方法设计模式
工厂模式定义:定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体工厂类中。
在学习工厂方法设计模式前,我们先了解了解简单的工厂模式,简单工厂模式不归属于23种设计模式中。
在简单工厂模式中创建实例的方法通常为静态(static)方法,因此简单工厂模式又称为静态工厂方法模式。
优点:
- 使得工厂和产品区分明确
- 客户无需知道所需创建具体产品的类名,只需知道参数
缺点:
- 扩展性差,增加新产品时不得不修改工作逻辑。
- 简单工厂模式会增加系统中类的个数,增加系统的复杂性和理解难度
- 简单工厂模式使用了 static 工厂方法,无法实现继承
- 违背了开闭原则
应用场景:
产品种类相对较少时,可以考虑使用简单工厂模式。
主要组成:
- 简单工厂(SimpleFactory),是简单工厂模式的核心,负责实现创建所有实例的内部逻辑,工厂类创建产品类的方法可以被外界直接调用,创建所需的产品对象
- 抽象产品(Product),是简单工厂创建所有对象的父类,负责描述所有实例共有的公共接口
- 具体产品(Concrete Product),是简单工厂模式的创建目标
模板代码:
// 简单工厂模式
public class Demo{
// 抽象产品
interface Product{
void show();
}
// 具体产品: Product A
static class ConcreteProduct1 implements Product{
@Override
public void show() {
System.out.println("具体产品1显示");
}
}
// 具体产品: Product B
static class ConcreteProduct2 implements Product{
@Override
public void show() {
System.out.println("具体产品2显示");
}
}
// 标记产品
final class Const{
final static int A = 0;
final static int B = 1;
}
static class SimpleFactory{
public static Product makeProduct(int type){
switch (type){
case Const.A:
return new ConcreteProduct1();
case Const.B:
return new ConcreteProduct2();
}
return null;
}
}
public static void main(String[] args) {
Product productA = SimpleFactory.makeProduct(Const.A);
Product productB = SimpleFactory.makeProduct(Const.B);
assert productA != null;
productA.show();
assert productB != null;
productB.show();
}
}
工厂方法模式定义:是对简单工厂模式的进一步抽象化,符合开闭原则
优点:
- 用户只需知道具体工厂名称就可以获得需要的产品,无须知道产品的具体创建过程
- 灵活性强,对于新产品创建,只需多写一个相应的工厂类
- 解耦合,符合迪米特法则、依赖倒置原则和里氏替换原则
缺点:
- 类个数容易增多,增加复杂度
- 增加了系统的抽象性和理解难度
- 抽象产品只能生产一种产品(可使用抽象工厂模式解决)
应用场景:
- 用户只需知道产品的工厂名,而不知道具体的产品名
- 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口
1.1 主要组成与模板代码
主要组成:
- 抽象工厂(Abstract Factory),提供了创建产品的接口,调用者通过它访问具体的工厂方法 newProduct() 来创建产品
- 具体工厂(ConcreteFactory),主要是实现抽象工厂中的抽象方法,完成具体产品的创建
- 抽象产品(Product),定义了产品的规范,描述了产品的主要特性和功能
- 具体产品(ConcreteProduct),实现了抽象产品角色所定义的接口,由具体的工厂创建
模板代码:
// 工厂方法模式
// 抽象产品
interface Product{
public void show();
}
// 具体产品 1: 实现抽象产品中的抽象方法
class ConcreteProduct1 implements Product{
@Override
public void show() {
System.out.println("具体产品1 展示");
}
}
// 具体产品2: 实现抽象产品中的抽象方法
class ConcreteProduct2 implements Product{
@Override
public void show() {
System.out.println("具体产品2 显示");
}
}
// 抽象工厂
interface AbstractFactory{
public Product newProduct();
}
// 具体工厂1 : 实现了产品1的生成方法
class ConcreteFactory1 implements AbstractFactory{
@Override
public Product newProduct() {
System.out.println("具体工厂1生成 --> 具体产品1...");
return new ConcreteProduct1();
}
}
// 具体工厂2: 实现了产品2的生成方法
class ConcreteFactory2 implements AbstractFactory{
@Override
public Product newProduct() {
System.out.println("具体工厂2生成 --> 具体产品2...");
return new ConcreteProduct2();
}
}
public class Demo{
public static void main(String[] args) {
ConcreteFactory1 factory1 = new ConcreteFactory1();
ConcreteFactory2 factory2 = new ConcreteFactory2();
Product product1 = factory1.newProduct();
Product product2 = factory2.newProduct();
product1.show();
product2.show();
}
}
1.2 应用案例
基于之前水果的例子,我们使用抽象类+泛型,将水果工厂类进一步划分成具体的水果工厂类
public abstract class FruitFactory<T extends Fruit> {
public abstract T getFruit();
}
class AppleFactory extends FruitFactory<Apple>{
@Override
public Apple getFruit() {
return new Apple();
}
}
class OriginFactory extends FruitFactory<Origin>{
@Override
public Origin getFruit() {
return new Origin();
}
}
二、抽象工厂模式
抽象工厂模式定义:是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无需指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式是之前工厂方法模式的升级版,后者只生产同一个等级的产品,而前者可生产多个等级的产品。
使用抽象工厂模式需满足的条件:
- 系统中有多组产品类型,每个具体工厂创建同一组但属于不同等级结构的产品
- 系统一次只可能消费其中某一组产品,即同组产品一起使用
优点:
- 具有工厂方法模式的所有优点
- 可共同管理产品组中的多等级产品
- 提高可扩展性,满足开闭原则
缺点:
- 当产品组里增加一个新产品时,所有的工厂类都需要进行修改,增加系统抽象性和理解难度。
应用场景:
- 当需要创建的对象是一系列相互关联或相互依赖的产品组时,如电器工厂中的电视机、洗衣机等
- 系统中有多个产品组,但每次只使用其中的某一组产品,如有人只喜欢买一个品牌的衣服和买
- 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
2.1 主要组成与模板代码
抽象工厂模式的主要组成:
- 抽象工厂(Abstract Factory),提供了创建产品的接口,包含多个创建产品的方法 newProduct(),可创建多个不同等级的产品
- 具体工厂(Concrete Factory),主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建
- 抽象产品(Product),定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品
- 具体产品(ConcreteProduct),实现了抽象产品角色所定义的接口,由具体工厂创建,同具体工厂是多对一的关系
模板代码:
1)抽象工厂:提供了产品的生成方法
interface AbstractFactory{
public Product1 newProduct1();
public Product2 newProduct2();
}
2)具体工厂:实现了产品的生成方法
class ConcreteFactory1 implements AbstractFactory{
public Product1 newProduct1{
System.out.println("具体工厂1 --> 具体产品a");
}
public Product2 newProduct2{
System.out.println("具体工厂2 --> 具体产品b");
}
}
2.2 应用案例
之前介绍的工厂方法模式,通过定义顶层抽象工厂类,以及继承的方式,针对每一种水果产品都提供一个工厂类进行创建,这里没有涉及更复杂的概念,比如工厂不仅生产水果,而且还生产饮料之类的。
这里我们可以定义水果、饮品类,然后抽象到一个工厂里,按品牌分类。
- 定义表示水果的类,和之前一样
public abstract class Fruit { // 水果抽象类
private final String name;
public Fruit(String name){
this.name = name;
}
@Override
public String toString() {
return name + "@" + hashCode(); // 打印当前水果名称和 哈希值
}
}
// 苹果
class Apple extends Fruit{
public Apple(){
super("苹果");
}
}
// 橘子
class Origin extends Fruit{
public Origin(){
super("橘子");
}
}
- 定义表示饮品的类,和之前表示水果类似
abstract class Drink { // 饮品抽象类
private final String name;
public Drink(String name){
this.name = name;
}
@Override
public String toString() {
return name + "@" + hashCode(); // 打印当前水果名称和 哈希值
}
}
// 苹果汁
class AppleDrink extends Drink{
public AppleDrink(){
super("苹果汁");
}
}
// 橘子汁
class OriginDrink extends Drink{
public OriginDrink(){
super("橘子汁");
}
}
- 定义抽象工厂类
abstract class AbstractFactory{
public abstract Fruit getFruit();
public abstract Drink getDrink();
}
class FactoryA extends AbstractFactory{
@Override
public Fruit getFruit() {
return new Apple();
}
@Override
public Drink getDrink() {
return new AppleDrink();
}
}
class FactoryB extends AbstractFactory{
@Override
public Fruit getFruit() {
return new Origin();
}
@Override
public Drink getDrink() {
return new OriginDrink();
}
}
- 测试:
public class Demo {
public static void main(String[] args) {
FactoryA factoryA = new FactoryA();
FactoryB factoryB = new FactoryB();
System.out.println("品牌A-> 水果: "+ factoryA.getFruit() +
", 饮品: " + factoryA.getDrink());
System.out.println("品牌B-> 水果: "+ factoryB.getFruit() +
", 饮品: " + factoryB.getDrink());
}
}
// 运行结果
// 品牌A-> 水果: 苹果@685325104, 饮品: 苹果汁@460141958
// 品牌B-> 水果: 橘子@1163157884, 饮品: 橘子汁@1956725890
优点:比工厂方法模式更清晰,能处理更加复杂的情况
缺点:不得不为每一个产品组的工厂都去添加新产品的生产方法,违背了开闭原则。
三、建造者模式
**定义:**指将一个复杂对象的构造与其实现分离,使同样的构件过程可以创建不同的实现对象。将一个复杂对象分解为多个简单对象,然后一步一步构件而成。
优点:
- 封装性好,构件和表示分离
- 扩展性好,各个具体的建造者相互独立,有利于系统的解耦
- 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其他模块产生影响
缺点:
- 产品的组成部分必须相同,限制了使用范围
与工厂模式对比:
- 关注点不同,建造者注重零部件组装过程,而工厂方法模式更注重零部件的创建过程,两者可以结合使用。
应用场景:
- 相同方法,不同的执行顺序,产生不同的结果
- 多个部件或零件,都可以装配到一个对象中,但产生的结果又不相同
- 产品类很复杂,或产品类中不同调用顺序产生不同的作用
- 初始化一个对象特别复杂,参数多,而且很多参数都有默认值
与工厂模式的区别:
- 侧重点不同,建造者模式注重方法调用顺序,工厂模式注重创建对象
- 创建结果不同,建造者模式创建复杂的对象,由复杂的部件组成,工厂模式创建的对象都一样
- 关注点不同,工厂模式只需要创建出对象,而建造者模式不仅要创建出对象,还要知道对象的组成部件
3.1 主要组成与模板代码
建造者模式的主要组成:
- 产品角色(Product),包含多个组件组成部件的复杂对象,由具体建造者来创建其各个零部件
- 抽象建造者(Builder),一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()
- 具体创建者(Concrete Builder),实现 Builder 接口,完成复杂产品的各个部件的具体创建方法
- 指挥者(Director),调用创建者对象中的部件构造,与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息
模板代码:
1)产品角色
class Product{
private String partA;
private String partB;
public void setPartA(String partA){
this.partA = partA;
}
public void setPartB(String partB){
this.partB = partB;
}
public void show(){
// 显示产品的特性
}
}
2)抽象建造者:包含创建产品各个子部件的抽象方法
abstract class Builder{
// 创建产品对象
protected Product product = new Product();
public abstract void buildPartA();
public abstract void buildPartB();
// 返回产品对象
public Product getResult(){
return product;
}
}
3)具体建造者:实现了抽象建造者的接口
// 具体建造者
class ConcreteBuilder extends Builder{
@Override
public void buildPartA() {
System.out.println("建造 PartA");
}
@Override
public void buildPartB() {
System.out.println("建造 PartB");
}
}
4)指挥者:调用建造者中的方法完成复杂对象的创建
// 指挥者
class Director{
private Builder builder;
public Director(Builder builder){
this.builder = builder;
}
// 产品构建与组装方法
public Product construct(){
builder.buildPartA();
builder.buildPartB();
return builder.getResult();
}
}
5)用户类
public class Demo{
public static void main(String[] args) {
Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
Product product = director.construct();
product.show();
}
}
3.2 应用案例
许多框架里用到的一种设计模式,JDK8本身有用的一种设计模式,比如 StringBuilder
:
这里我们先创建一个StringBuilder对象,然后就可以链式的添加元素。
public class Demo {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
builder.append("Hello, ").append("World!");
System.out.println(builder);
}
}
接下来先定义一个学生实体类,然后通过之前的方法来创建学生实例。
import java.util.List;
public class Student {
private String name;
private int age;
private List<String> hobbies;
public Student(){}
public Student(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
this.hobbies = hobbies;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public List<String> getHobbies() {
return hobbies;
}
public void setHobbies(List<String> hobbies) {
this.hobbies = hobbies;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", hobbies=" + hobbies +
'}';
}
}
在创建学生实例的时候,主要有两种情况:
- 创建具有所有属性的学生对象
- 创建只有部分属性的学生对象
import java.util.ArrayList;
import java.util.Arrays;
public class Demo {
public static void main(String[] args) {
// 创建所有属性
Student a = new Student("uni", 22, Arrays.asList("吃饭", "睡觉"));
// 创建部分属性
Student b = new Student();
b.setName("uni");
b.setAge(22);
System.out.println("a: " + a);
System.out.println("b: " + b);
}
}
// 运行结果
// a: Student{name='uni', age=22, hobbies=[吃饭, 睡觉]}
// b: Student{name='uni', age=22, hobbies=null}
可以看到无论是所有属性还是部分属性,都不太方便。
所有属性构造,需要通过构造函数,填入对应的参数,参数位置不能改变。
部分属性,需要先构造空的对象,然后逐一的set,这样使得代码看起来十分不美观,最好的策略就是在创建对象的时候,就直接能将我们需要填入的参数赋值给这个对象。接下来我们通过建造者模式来解决这个问题。
这里由于 getter、setter方法比较多,有些冗余,所以使用 lombok 的注解来简化代码。
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@ToString
public class Student {
private String name;
private int age;
private List<String> hobbies;
public Student(){}
private Student(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
this.hobbies = hobbies;
}
public static StudentBuilder builder(){
return new StudentBuilder();
}
@Getter
public static class StudentBuilder{
private String name;
private int age;
private List<String> hobbies;
public StudentBuilder name(String name) {
this.name = name;
return this;
}
public StudentBuilder age(int age) {
this.age = age;
return this;
}
public StudentBuilder hobbies(List<String> hobbies) {
this.hobbies = hobbies;
return this;
}
public Student build(){
return new Student(name, age, hobbies);
}
}
}
通过上述代码可以看到,Student的全参构造设置为了 private 私有级别,这使得在创建Student实例时只能通过其内部的静态类 StudentBuilder,该内部类存储了 Student对象的所有属性,方便在构造的时候进行赋值。
静态内部类 StudentBuilder提供返回实例对象的set方法,以及 build 方法返回一个Student对象,这里的build方法则固定了参数的位置,所以在调用的时候无需关系参数的顺序。
import java.util.Arrays;
public class Demo {
public static void main(String[] args) {
// 创建所有属性
Student a = new Student.StudentBuilder()
.name("uni")
.age(22)
.hobbies(Arrays.asList("吃饭", "睡觉"))
.build();
// 创建部分属性
Student b = new Student.StudentBuilder()
.name("uni")
.age(22)
.build();
System.out.println("a: " + a);
System.out.println("b: " + b);
}
}
// 运行结果
// a: Student(name=uni, age=22, hobbies=[吃饭, 睡觉])
// b: Student(name=uni, age=22, hobbies=null)
四、单例模式
**定义:**单例模式是指只有一个实例对象,同一个类始终只会有一个对象来进行操作,比如数据库连接类,这种情况下只需要创建一个对象或者直接使用静态方法,不需要创建多个对象。
优点:
- 单例模式可保证内存中只有一个实例,减少了内存的开销
- 可以避免对资源的多重占用
- 单例模式设置全局访问点,可以优化和共享资源的访问
缺点:
- 单例模式一般没有接口,扩展困难,扩展只能修改原来的代码,违背了开闭原则
- 单例模式的功能代码通常写在一个类中,容易违背单一职责原则
应用场景:
- 需频繁创建的一些类可以用单例模式
- 需要频繁实例化的类,而创建的对象又被频繁销毁时,如多线程的线程池
- 当对象需要被共享,单例模式只允许创建一个对象,共享该对象可节省内存
4.1 主要组成与模板代码
单例的主要组成:
- 单例类:包含一个实例且能自行创建这个实例的类
- 访问类:使用单例的类
单例模式通常分为两种,饿汉式和懒汉式。
1. 饿汉式
public class Singleton {
// 饿汉式: 定义静态对象,并创建实例
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
创建饿汉式的实例:
class Demo{
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
}
}
// 运行结果
// Singleton@4554617c
2. 懒汉式
public class Singleton {
// 懒汉式: 先定义静态对象,但不创建实例
private static Singleton instance;
private Singleton(){}; // 构造方法私有化
public static Singleton getInstance(){
if(instance == null)
instance = new Singleton();
return instance;
}
}
3. 懒汉式的多线程定义
之前的定义是对于单线程情景的,如果我们是使用多线程来创建实例,那么就需要考虑并发带来的问题。假如同时有两个线程判断了 if(instance == null) ,那么 instance就将被这两个线程实例化,这样依赖就实例化了两次,在高并发场景,有多个线程,那么instance可能会被实例化很多次。
比如:我们使用三个线程来创建实例,如果对三个线程分别设置 1s 、2s 、3s 的延迟,达到延迟后就创建懒汉式的实例,这种情况下,Singleton被实例化了一次。
class Demo{
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(1000);
Singleton singleton = Singleton.getInstance();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread thread2 = new Thread(() -> {
try{
Thread.sleep(2000);
Singleton singleton = Singleton.getInstance();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread thread3 = new Thread(() -> {
try{
Thread.sleep(3000);
Singleton singleton = Singleton.getInstance();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread1.start();
thread2.start();
thread3.start();
}
}
但我们将线程的延迟时间都调整到100s,这时候Singleton就被每一个线程实例化了一次,即共实例化了三次。
Thread.sleep(100);
所以我们需要用锁来预防这种情况,防止对象被多次实例化,浪费资源。
4. synchronized 同步代码块
方法一:同步创建实例的方法,添加 synchronized 关键字
public class Singleton {
private static Singleton instance;
private Singleton(){}; // 构造方法私有化
public static synchronized Singleton getInstance(){
if(instance == null) {
System.out.println("Singleton被实例化了.");
instance = new Singleton();
}
return instance;
}
}
给getInstance()方法添加synchronized关键字之后,多个线程就无法并发的执行这个方法了,这样就有效地避免了多次创建实例的情况,这里可以继续使用之前的多线程代码进行测试,可以将3个线程sleep的时间设置为相同的100,然后同时启动3个线程,结果肯定是只实例化了一次。
这种方式是最简单粗暴的,直接锁整个方法,但是也有一个缺点那就是效率比较低,因为在其他线程执行到这个方法的时候,会等待锁释放,然后才执行方法,这意味着getInstance() 方法还是被其他的线程执行了,只是它们没有创建实例而已。
5. synchronized 同步创建实例
根据之前的问题,synchronized 同步整个方法,会导致其他线程等待锁的释放,同时还会执行这个方法,比较浪费资源,这里将synchronized 定义到判断实例为空的内部,相当于只锁住创建实例的部分,因为执行时间更短,故性能会稍微高一些。
public class Singleton {
private static Singleton instance;
private Singleton(){}; // 构造方法私有化
public static Singleton getInstance(){
if(instance == null) {
synchronized(Singleton.class){
instance = new Singleton();
System.out.println("Singleton被实例化了.");
}
}
return instance;
}
}
不过这里依然有之前的问题,多个线程在判断 instance == null 成立后,有可能同时进入同步代码块,这就跟之前一样,出现了创建多次实例的情况,这里的优化思路是在同步代码块里再进行一次判断,这样避免重复创建实例。
public class Singleton {
private static Singleton instance;
private Singleton(){}; // 构造方法私有化
public static Singleton getInstance(){
if(instance == null) {
synchronized(Singleton.class){
if(instance == null) {
instance = new Singleton();
System.out.println("Singleton被实例化了.");
}
}
}
return instance;
}
}
最后这里还可以进行优化,那就是将 instance 加一个关键字 volatile,这个关键字可保证 instance 在多个线程之间可见,这样当其他线程进入同步代码块后,会拿其他线程更新的最新值去判断(这里理解不是很深入,大概先这么认为)
private static volatile Singleton instance;
6.【最优策略】静态内部类实现懒汉式
这里是根据Java类的加载特性,静态内部类在调用之前是不会进行初始化的,这样我们只要在静态内部类里先初始化一个静态实例,之后在获取的时候直接运行然后返回,这样就保证获取的实例是同一个而且仅有一个了。
public class Singleton {
private Singleton(){} // 私有化无参构造
private static class Holder{ // 静态内部类
// 根据类加载特性, 仅使用Singleton类时,不会对静态内部类进行初始化
private final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return Holder.INSTANCE; // 直接获取内部类的静态实例
}
}
接下来我们通过一个简单的多线程Demo来验证这种懒汉式的正确性:
class Demo{
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(100);
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread thread2 = new Thread(() -> {
try{
Thread.sleep(100);
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread1.start();
thread2.start();
}
}
// 运行结果
// Singleton@2b139240
// Singleton@2b139240
在多线程中获取实例,然后输出其哈希的部分值,这样可以看到输出的哈希结果是一样的,所以指向的就是同一个对象实例。
而且没有任何加锁的操作,同时也保证了线程的安全。
五、 原型模式
**定义:**用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
优点:
- Java 自带的原型模式基于内存二进制流的复制,在性能上比直接new一个对象更好
- 可使用深拷贝保存对象,简化创建对象的过程
缺点:
- 需为每一个类都配置 clone 方法
- clone 方法位于类的内部,当对已有类进行编写时需修改代码,违背了开闭原则
- 深拷贝时需编写复杂的代码,实现较为麻烦。
原型模式实际上与对象拷贝息息相关,原型模式使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。
即,原型对象作为模板,通过克隆操作,来产生更多的对象,就像细胞复制。
应用场景:
- 对象之间相同或相似,只是个别属性不同的时候
- 创建对象成本大,如初始化时间长,占用CPU太多,或占用网络资源多等
- 创建一个对象需频繁的数据准备或访问权限等,需提高性能或提高安全性
- 系统中大量使用该类对象,且各个调用者都需给它的属性重新赋值
5.1 主要组成与模板代码
原型模式主要组成:
- 抽象原型类,规定了具体原型对象必须实现的接口
- 具体原型类,实现了抽象类的 clone() 方法,是可被复制的对象
- 访问类,使用具体原型中的 clone() 方法 来复制新的对象
1. 浅拷贝
对于类中的基本数据类型,赋值时会直接复制值给拷贝的对象;
对于引用类型,只会复制对象的地址,而实际上还是原来的那个对象,相当于变脸术。
public class Copy {
public static void main(String[] args) {
int a = 10;
int b = a; // 基本类型 浅拷贝
System.out.println(a == b); // 结果为: true
Object obja = new Object();
Object objb = obja; // 引用类型 浅拷贝引用
System.out.println(obja == objb); // 结果为: true
}
}
2. 深拷贝
无论是基本类型还是引用类型,深拷贝会将引用类型的所有内容,全部拷贝为一个新的对象,包括对象内部的所有成员变量,也进行拷贝。
在 Java 中,我们可以通过 Cloneable 接口提供的拷贝机制,来实现原型模式:
public class Student implements Cloneable{ // 必须实现 Cloneable 接口
@Override
public Student clone() {
try {
return (Student) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
测试看看克隆的对象是否为原来的对象:
class Demo{
public static void main(String[] args) {
Student a = new Student();
Student b = a.clone();
System.out.println(a);
System.out.println(b);
}
}
// 运行结果
// Student@4554617c
// Student@74a14482
可以看到,输出的哈希值不同,所以是不同的对象,克隆成功,但不能确认这就是深拷贝,因为深拷贝要保证其内部的引用类型都各不相同。
内部属性的深拷贝
基于上一个例子,我们在 Student 类里定义引用类型 String,然后通过克隆创建不同的Student 实例,对它们的 Name 进行比较,看看是否为同一个 String。
public class Student implements Cloneable{
private String name;
public Student(String name){
this.name = name;
}
public String getName(){
return name;
}
@Override
public Student clone() {
try {
return (Student) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
查看内部属性是否克隆成功
class Demo{
public static void main(String[] args) {
Student a = new Student("a");
Student b = a.clone();
System.out.println(a.getName() == b.getName()); // 结果为: true
}
}
由于返回的结果为true,所以内部属性Name克隆失败了,引用类型还是之前的克隆对象。
所以Java提供的clone方法实际上是一种浅拷贝,其内部的引用对象并不会进行拷贝。
要实现真正的深拷贝,在clone()方法里,我们需要拷贝每一个引用类型,这里需要拷贝 name。
@Override
public Student clone() {
try {
Student student = (Student) super.clone();
student.name = new String(name);
return student;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}