23种设计模式之创建型模式

一.创建型模式关注点是如何创建对象,其核心思想是要把对象的创建和使用相分离,这样使得两者能相对独立地变换。

二.创建型模式包括:

  • 工厂方法:Factory Method
  • 抽象工厂:Abstract Factory
  • 建造者:Builder
  • 原型:Prototype
  • 单例:Singleton

1.工厂方法

定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。
工厂方法的目的是使得创建对象和使用对象是分离的,并且客户端总是引用抽象工厂和抽象产品:

参照具体的例子来说:假设我们希望实现一个解析字符串到NumberFactory,可以定义如下:

public interface NumberFactory {
    Number parse(String s);
}

有了工厂接口,再编写一个工厂的实现类:

public class NumberFactoryImpl implements NumberFactory {
    public Number parse(String s) {
        return new BigDecimal(s);
    }
}

而产品接口是NumberNumberFactoryImpl返回的实际产品是BigDecimal
那么客户端如何创建NumberFactoryImpl呢?通常我们会在接口Factory中定义一个静态方法getFactory()来返回真正的子类:

public interface NumberFactory {
    // 创建方法:
    Number parse(String s);

    // 获取工厂实例:
    static NumberFactory getFactory() {
        return impl;
    }

    static NumberFactory impl = new NumberFactoryImpl();
}

在客户端中,我们只需要和工厂接口NumberFactory以及抽象产品Number打交道:

NumberFactory factory = NumberFactory.getFactory();
Number result = factory.parse("123.456");

调用方可以完全忽略真正的工厂NumberFactoryImpl和实际的产品BigDecimal,这样做的好处是允许创建产品的代码独立地变换,而不会影响到调用方。
有的人会问:一个简单的parse()需要写这么复杂的工厂吗?实际上大多数情况下我们并不需要抽象工厂,而是通过静态方法直接返回产品,即:

public class NumberFactory {
    public static Number parse(String s) {
        return new BigDecimal(s);
    }
}

这种简化的使用静态方法创建产品的方式称为静态工厂方法(Static Factory Method)。静态工厂方法广泛地应用在Java标准库中.
例子:
Integer n = Integer.valueOf(100);
Integer既是产品又是静态工厂。它提供了静态方法valueOf()来创建Integer。那么这种方式和直接写new Integer(100)有何区别呢?我们观察valueOf()方法:

public final class Integer {
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    ...
}

它的好处在于,valueOf()内部可能会使用new创建一个新的Integer实例,但也可能直接返回一个缓存的Integer实例。对于调用方来说,没必要知道Integer创建的细节。

工厂方法可以隐藏创建产品的细节,且不一定每次都会真正创建产品,完全可以返回缓存的产品,从而提升速度并减少内存消耗。
如果调用方直接使用Integer n = new Integer(100),那么就失去了使用缓存优化的可能性。
我们经常使用的另一个静态工厂方法是List.of():
List<String> list = List.of("A", "B", "C");
这个静态工厂方法接收可变参数,然后返回List接口。需要注意的是,调用方获取的产品总是List接口,而且并不关心它的实际类型。即使调用方知道List产品的实际类型是java.util.ImmutableCollections$ListN,也不要去强制转型为子类,因为静态工厂方法List.of()保证返回List,但也完全可以修改为返回java.util.ArrayList。这就是里氏替换原则:返回实现接口的任意子类都可以满足该方法的要求,且不影响调用方。
总是引用接口而非实现类,能允许变换子类而不影响调用方,即尽可能面向抽象编程。

小结

工厂方法是指定义工厂接口和产品接口,但如何创建实际工厂和实际产品被推迟到子类实现,从而使调用方只和抽象工厂与抽象产品打交道。
实际更常用的是更简单的静态工厂方法,它允许工厂内部对创建产品进行优化。
调用方尽量持有接口或抽象类,避免持有具体类型的子类,以便工厂方法能随时切换不同的子类返回,却不影响调用方代码。

2.抽象工厂:

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
抽象工厂模式和工厂方法不太一样,它要解决的问题比较复杂,不但工厂是抽象的,产品是抽象的,而且有多个产品需要创建,因此,这个抽象工厂会对应到多个实际工厂,每个实际工厂负责创建多个实际产品:
这种模式有点类似于多个供应商负责提供一系列类型的产品。我们举个例子:
假设希望为商家提供一个生产手机和PC的服务,定义接口如下:

public interface AbstractFactory {
    //创建手机
    phone createPhone();
    //创建pc
    pc createPc();
}

注意到上面的抽象工厂仅仅是一个接口,没有任何代码。同样的,因为phone和pc都比较复杂,现在我们并不知道如何实现它们,所以只有接口:

interface phone {
    String phone();
}

interface pc{
    String pc();
}

这样,我们就定义好了抽象工厂(AbstractFactory)以及两个抽象产品(phonepc)。因为实现它们比较困难,我们决定让供应商来完成。
现在市场上有两家供应商:小米和苹果两个品牌,我们决定同时使用这两家供应商的产品.
我们先看看apple的产品是如何实现的。首先,apple必须要有实际的产品,即ApplePhoneApplePc

public class ApplePhone implements phone{
    @Override
    public String phone() {
        return "生产了一个iphone";
    }
}
class ApplePc implements pc{
    @Override
    public String pc() {
        return "生产了一个Mac";
    }
}

然后,apple必须提供一个实际的工厂来生产这两种产品,即AppleFactory:

public class AppleFactory implements AbstractFactory{
    @Override
    public phone createPhone() {
        return new ApplePhone();
    }

    @Override
    public pc createPc() {
        return new ApplePc();
    }
}

这样,我们就可以使用apple的服务了。客户端编写代码如下:

public class test {
    public static void main(String[] args) {
//创建AbstractFactory ,实际是AppleFactory类型
        AbstractFactory appleFactory = new AppleFactory();
        phone iphone = appleFactory.createPhone();
        System.out.println(iphone.phone());
        pc pc = appleFactory.createPc();
        System.out.println(pc.pc());
      }
}

运行结果:

如果我们要同时使用Mi的服务怎么办?因为用了抽象工厂模式,Mi只需要根据我们定义的抽象工厂和抽象产品接口,实现自己的实际工厂和实际产品即可:

public class MiFactory implements AbstractFactory {
    @Override
    public phone createPhone() {
        return new MiPhone();
    }

    @Override
    public pc createPc() {
        return new MiPc();
    }
}
public class MiPhone implements phone{

    @Override
    public String phone() {
        return "生产了一个MiPhone";
    }
}
class MiPc implements pc{

    @Override
    public String pc() {
        return "生产了一个MiPC";
    }
}

客户端要使用mi的服务,只需要把原来的new AppleFactory()切换为new MiFactory()即可。

public class test {
    public static void main(String[] args) {
        AbstractFactory miFactory = new MiFactory();
        phone Miphone = miFactory.createPhone();
        System.out.println(Miphone.phone());
        pc Mipc = miFactory.createPc();
        System.out.println(Mipc.pc());
  }
}

结果:
注意到客户端代码除了通过new创建了AppleFactory或MiFactory外,其余代码只引用了产品接口,并未引用任何实际产品(例如,ApplePhone ),如果把创建工厂的代码放到AbstractFactory中,就可以连实际工厂也屏蔽了:

public interface AbstractFactory {
    //创建手机
    phone createPhone();
    //创建pc
    pc createPc();

public static AbstractFactory createAbstractFactory(String name){
        if (name.equalsIgnoreCase("apple")){
            return new AppleFactory();
        }else if (name.equalsIgnoreCase("mi")){
            return new MiFactory();
        }else{
            throw new IllegalArgumentException("Invalid factory name");
        }
    }
}

客户端:

AbstractFactory apple = AbstractFactory.createAbstractFactory("apple");
        phone applePhone = apple.createPhone();
        pc applePc = apple.createPc();
        System.out.println(applePhone.phone());
        System.out.println(applePc.pc());

小结:抽象工厂模式是为了让创建工厂和一组产品与使用相分离,并可以随时切换到另一个工厂以及另一组产品;

抽象工厂模式实现的关键点是定义工厂接口和产品接口,但如何实现工厂与产品本身需要留给具体的子类实现,客户端只和抽象工厂与抽象产品打交道。

3.建造者:

定义
  将一个复杂对象的构建与它的表示分离,使得同样的构建过程可创建不同的表示。

使用场景
相同的方法,不同的执行顺序

要初始化的对象十分复杂,如参数多且都具有默认值
例子:

  • 组装电脑的配置,组装电脑的配置
  •  肯德基的套餐配置
  •  Java中的StringBuilder
  •  ps组合使用:工厂模式建造零件,建造者模式创建复杂对象)
    优缺点
      优点:

良好的封装性,可使调用者不必知道产品内部组成的细节。(控制细节风险)
建造者独立,容易扩展。
  缺点
会产生多余的Builder对象以及Director对象,消耗内存。
电脑组装例子:
首先创建实体

import lombok.Data;

@Data
public class Computer{
    private String buildA;//内存
    private String buildB;//显示屏
    private String buildC;//CPU
    private String buildD;//键鼠

    @Override
    public String toString() {
        return "Computer{" +
                "buildA='" + buildA + '\'' +
                ", buildB='" + buildB + '\'' +
                ", buildC='" + buildC + '\'' +
                ", buildD='" + buildD + '\'' +
                '}';
    }
}

创建动作父类(abstract抽象父类)

public abstract class Builder {
    //无参构造,均设置好默认值
    abstract void buildA();//内存
    abstract void buildB();//显示屏
    abstract void buildC();//CPU
    abstract void buildD();//键鼠

    //有参构造,DIY
    abstract void buildA(String str);
    abstract void buildB(String str);

    abstract Computer getComputer();
}

写多个子类,重写父类方法:

public class Worker_Normal extends Builder{

    private Computer computer;

    public Worker_Normal() {
        computer = new Computer();
    }

    @Override
    void buildB() {
        computer.setBuildB("1080p显示屏");
        System.out.print("1080p显示屏==>");
    }

    @Override
    void buildA() {
        computer.setBuildA("1T内存");
        System.out.print("1T内存==>");
    }

    @Override
    void buildC() {
        computer.setBuildC("i5");
        System.out.print("i5==>");
    }

    @Override
    void buildD() {
        computer.setBuildD("联想键鼠");
        System.out.print("联想键鼠==>");
    }

    @Override
    void buildA(String str) {
        computer.setBuildA(str);
    }

    @Override
    void buildB(String str) {
        computer.setBuildB(str);
    }

    @Override
    Computer getComputer() {
        return computer;
    }
}
public class Worker_Good extends Builder{

    private Computer computer;

    public Worker_Good() {
        computer = new Computer();
    }

    @Override
    void buildA() {
        computer.setBuildA("2T内存");
    }

    @Override
    void buildB() {
        computer.setBuildB("2K显示屏");
    }

    @Override
    void buildC() {
        computer.setBuildC("i9");
    }

    @Override
    void buildD() {
        computer.setBuildD("罗技键鼠");
    }

    @Override
    void buildA(String str) {
        computer.setBuildA(str);
    }

    @Override
    void buildB(String str) {
        computer.setBuildB(str);
    }

    @Override
    Computer getComputer() {
        return computer;
    }
}

写Director,决定顺序和组件个数:

public class Director {
    public Computer build(Builder builder){//director的作用很重要,决定具体顺序
        builder.buildA();
        builder.buildB();
        builder.buildC();
        builder.buildD();

        return builder.getComputer();
    }
}
public class DirectorPRO {
    public Computer build(Builder builder){
        builder.buildA();
        builder.buildA();
        builder.buildD();
        builder.buildC();
        builder.buildB();

        return builder.getComputer();
    }
}

最后写测试类

public class Test {
    public static void main(String[] args) {

        System.out.println("====标配高级电脑====");
        Director director = new Director();
        Worker_Good worker_good = new Worker_Good();
        Computer buildGood = director.build(worker_good); //Alt+Enter自动补全
        System.out.println(buildGood.toString() );

        System.out.println("====DIY高级电脑====");//设置一些有参构造,可以DIY属性; 无参构造只能是默认属性
        Computer buildGoodDIY = director.build(worker_good);
        worker_good.buildA("DIY:4T内存");
        worker_good.buildB("DIY:4K显示屏");
        System.out.println(buildGoodDIY.toString() );

        System.out.println("====普通电脑====");
        for (int i=0;i<2;i++){
            Computer buildNormal = director.build(new Worker_Normal());
            System.out.println(buildNormal.toString());
        }
        System.out.println("====普通电脑PRO====");//这里用了不同的director可使构造的顺序不同(director决定顺序,还有组件数量)
        DirectorPRO directorPRO = new DirectorPRO();
        for(int i=0;i<1;i++){
            Computer buildNormalPro = directorPRO.build(new Worker_Normal());
            System.out.println(buildNormalPro);//这里应该是帮助自动toString
            System.out.println(buildNormalPro.toString());
        }
    }
}

运行结果:

类图:

小结

Builder模式是为了创建一个复杂的对象,需要多个步骤完成创建,或者需要多个零件组装的场景,且创建过程中可以灵活调用不同的步骤或组件。

4.原型:

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。->(对象的克隆)
优点:

  • 克隆对象会将对象已经设置的属性带出来,而不用在new之后去一个个重新设置。
  • Java 自带的原型模式基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良。
  • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
    例子:
import lombok.*;
import java.util.Date;

//@Data  // Lombok的@Data注解有侵入性,会重写hashcode,导致clone对象hash值一致
@AllArgsConstructor
@Setter
@Getter
public class Video implements Cloneable{ //implements Cloneable
    private String name;
    private Date createTime;

    @Override
    public String toString() { //手动增加打印hashcode
        return  "hashCode = " + super.hashCode() +
                " Video{" +
                " name='" + name + '\'' +
                ", createTime=" + createTime +
                '}';
    }

    //Alt+Insert => Override =>clone()
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();//父类克隆
    }
}

测试:

import java.util.Date;

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException, InterruptedException {
        Date date = new Date();
        Video video1 = new Video("【流浪地球】", date);
        System.out.println("video1 = " + video1);

        Video video2 = (Video) video1.clone(); //类型强制转换
        System.out.println("video2 = " + video2);

        video2.setName("【学习视频】");
        System.out.println("video1 = " + video1);
        System.out.println("video2 = " + video2);
    }
}

结果:

分析:

clone出来的对象的hashCode和原对象是不同的;
修改属性后,两个对象就不同了,说明不是引用,而是真实复制了副本;
加入自定义类:

import lombok.*;

@AllArgsConstructor
@Getter
@Setter
public class ClonePeople implements Cloneable{
    private String name;
    private Integer height;
    private Integer Intelligent;
    private BaseInfo baseInfo;//自定义类

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "hashCode" + super.hashCode() +
                " ClonePeople{" +
                "name='" + name + '\'' +
                ", height=" + height +
                ", Intelligent=" + Intelligent +
                ", baseInfo=" + baseInfo +
                '}';
    }
}

写一个自定义类:

import lombok.*;

@Setter
@Getter
@AllArgsConstructor
@ToString
public class BaseInfo {
    private String address;
    private String parents;
}

测试:

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        ClonePeople clonePeople = new ClonePeople("双胞胎哥哥",195,155, new BaseInfo("地球","超人"));
                                                           //有参构造中,对象的值格式(value1 , value2) =>括号逗号

        System.out.println("OriginPeople = " + clonePeople);//这里clonePeople等价于clonePeople.toString()
//        System.out.println("clonePeople = " + clonePeople.toString());

        ClonePeople cloneLittleBrother = (ClonePeople) clonePeople.clone();//纯克隆,只有hashCode改变
        System.out.println("clone = " + cloneLittleBrother);

        //设置新属性值
        cloneLittleBrother.setName("双胞胎弟弟");
        cloneLittleBrother.setIntelligent(175);
        cloneLittleBrother.setBaseInfo(new BaseInfo("海王星","超人"));
        System.out.println("======最终结果======");
        System.out.println("cloneLittleBrother = " + cloneLittleBrother);
        System.out.println("OriginPeople = " + clonePeople.toString());
    }
}

结果:

5.单例:

单例模式:一个类只能构造一个实例对象("构造器私有")
场景:

  • Windows任务管理器、回收站

  • 项目中,配置文件的类,一般只有一个对象

  • 网站的计数器、时钟

  • 数据库连接池

  • Servlet

  • Spring中的Bean(缓存中取bean很快,减少jvm垃圾回收)(当有请求来的时候会先从缓存(map)里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象)

a.饿汉式单例->上来直接new对象,所有类实例化。坏处是:大量浪费不必要的资源(因为很多类 不需要实例化)

public class Hungry {
    private Hungry(){} //构造函数
    private final static Hungry HUNGRY = new Hungry();//直接实例化,new出对象
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

b.懒汉式单例->懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。

线程不安全:多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题(Synchronized 同步锁来修饰 getInstance 方法)

public class Singleton {
    private Singleton() {}  //单例=》私有构造器
    private static Singleton instance = null;  //单例对象
    public static Singleton getInstance() {  //调用getInstance方法才会开始构造对象
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

但是这种方法不符合线程安全:

public class Singleton {
    private Singleton() {
        System.out.println(Thread.currentThread().getName()); //构造时候打印线程名
    }  //私有构造函数
    private static Singleton instance = null;  //单例对象
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    public static void main(String[] args) {
        for(int i =0;i<10;i++){
            new Thread(  ()->{Singleton.getInstance();}  ).start();  //1.启动新线程 2.lambda表达式
        }
    }
}

可能会出现构造了很多个的情形

方法一:双重锁检测 DCL->==》首先将类加同步锁(syn),但是new语句不是原子操作,所以对了类的实例加volatile锁(可见性)

第一把锁:synchronized锁

public static Singleton getInstance() {
    if (instance == null) {     //外层判断
        synchronized (Singleton.class) {    //将Singleton类 加锁
            if (instance == null) {     //原有判断
                instance = new Singleton();          // 上面这行 不是原子操作:          // 1.分配内存空间  2.执行构造方法,初始化对象  3.把这个对象指向这个空间
            }
        }
    }
    return instance;
}//加上两层:一层if、一层锁
//这样就能保证单例在多线程下的唯一性
//双重检查模式:DCL (Double Check)

第二把锁:volatile锁
private volatile static Singleton instance = null; //由于instance = new Singleton()的非原子性(new对象3步),所以需要volatile保证强制同步一致
volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值。

但是可以用反射来破坏DCL:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Singleton instance = Singleton.getInstance();
        Constructor<Singleton> declaredConstructor =Singleton.class.getDeclaredConstructor(null);  //通过反射获取declaredConstructor这个构造器
        declaredConstructor.setAccessible(true);
        Singleton instance2 = declaredConstructor.newInstance();

        System.out.println(instance.hashCode() );
        System.out.println(instance2.hashCode() );
    }

实现单例模式,三种方法对比:

方法二:用静态内部类

public class Holder {
    private Holder(){}

    public static Holder getInstance() {return InnerClass.HOLDER;}

    private static class InnerClass{
        private static final Holder HOLDER = new Holder();
    }
}

这个方法也能被反射破坏单例的唯一性

方法三:枚举->直接把类名前的class替换成enum就好了,因为枚举无法反射

public enum EnumSingleton { //这里是enum而不是class
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

测试:

class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingleton instance1 = EnumSingleton.INSTANCE.getInstance();

        Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingleton instance2 = declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

结果:

异常:枚举无法创造反射.

小结

Singleton模式是为了保证一个程序的运行期间,某个类有且只有一个全局唯一实例;

Singleton模式既可以严格实现,也可以以约定的方式把普通类视作单例。

posted @   我是一个邓疯子  阅读(233)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
ヾ(≧O≦)〃嗷~,

这是回到顶部的路哦

喜欢请打赏

扫描二维码打赏

了解更多

点击右上角即可分享
微信分享提示