查看原文
其他

2.5 万字详解:23 种设计模式

王德印 Java后端 2022-09-05
本文简述了各大设计模式,并通过UML和代码详细说明。本文大约共 2.5W 字,建议收藏。下方是本文的目录:

一、设计模式的认识

二、设计模式的分类

  • 根据其目的
  • 根据范围

三、设计模式的优点

四、设计模式中关键点

五、创建型模式

  • 简单(静态)工厂模式
  • 工厂方法模式
  • 抽象工厂模式
  • 单例模式
  • 原型模式
  • 建造者模式

六、个人体会

一、设计模式的认识

设计模式(Design Pattern)是前辈们经过相当长的一段时间的试验和错误总结出来的,是软件开发过程中面临的通用问题的解决方案。这些解决方案使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。

二、设计模式的分类

(1)根据其目的

即模式是用来做什么的,可分为创建型(Creational),结构型(Structural)和行为型(Behavioral)三种:①创建型模式主要用于创建对象。②结构型模式主要用于处理类或对象的组合。③行为型模式主要用于描述对类或对象怎样交互和怎样分配职责。

(2) 根据范围

即模式主要是用于处理类之间关系还是处理对象之间的关系,可分为类模式和对象模式两种:类模式处理类和子类之间的关系,这些关系通过继承建立,在编译时刻就被确定下来,是属于静态的。对象模式处理对象间的关系,这些关系在运行时刻变化,更具动态性。

三、设计模式的优点

①可以提高程序员的思维能力、编程能力和设计能力。②使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。③使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。现在这样说肯定有些懵逼,需要在实际开发中才能体会得到真正的好处

四、设计模式中关键点

(1)创建型模式简单工厂:一个工厂类根据传入的参量决定创建出那一种产品类的实例。工厂方法:定义一个创建对象的接口,让子类决定实例化那个类。

抽象工厂:创建相关或依赖对象的家族,而无需明确指定具体类。

建造者模式:封装一个复杂对象的构建过程,并可以按步骤构造。

单例模式:某个类只能有一个实例,提供一个全局的访问点。

原型模式:通过复制现有的实例来创建新的实例。

(2)结构型模式

外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。

桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。

组合模式:将对象组合成树形结构以表示“”部分-整体“”的层次结构。

装饰模式:动态的给对象添加新的功能。

代理模式:为其他对象提供一个代理以便控制这个对象的访问。

适配器模式:将一个类的方法接口转换成客户希望的另外一个接口。

亨元(蝇量)模式:通过共享技术来有效的支持大量细粒度的对象。

(3)行为型模式模板模式:定义一个算法结构,而将一些步骤延迟到子类实现。

解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。

策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。

状态模式:允许一个对象在其对象内部状态改变时改变它的行为。

观察者模式:对象间的一对多的依赖关系。

备忘录模式:在不破坏封装的前提下,保持对象的内部状态。

中介者模式:用一个中介对象来封装一系列的对象交互。

命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。

访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能。

责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。

迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。

五、创建型模式

(1)简单(静态)工厂模式

1.认识

①一句话来说就是,一个工厂类根据传入的参量决定创建出那一种产品类的实例。因为逻辑实现简单,所以称为简单工厂模式,也因为工厂中的方法一般设置为静态,所以也称为静态工厂,它不属于23种模式。

②简单工厂模式专门定义一个工厂类来负责创建其他类的实例,被创建的实例通常都具有共同的父类,在工厂类中,可以根据参数的不同返回不同类的实例。升级版本简单工厂模式,通过反射根据类的全路径名生成对象。

③简单工厂模式就是将这部分创建对象语句分离出来,由工厂类来封装实例化对象的行为,修改时只需要修改类中的操作代码,使用时调用该类不需要考虑实例化对象的行为,使得后期代码维护升级更简单方便,有利于代码的可修改性与可读性。

④但是如果增加新的产品的话,需要修改工厂类的判断逻辑,违背开闭原则。

2.UML图解

简单介绍一下UML:泛化:继承 带三角箭头的实线,箭头指向类

实现:实现 带三角箭头的虚线,箭头指向接口

依赖:new A的对象当作方法参数传递进来作为B类的局部变量 带箭头的虚线,指向被使用者

关联:一个类作为另一个类的成员变量 带普通箭头的实心线,指向被拥有者

聚合:new A的对象当作方法参数传递进来作为B类的成部变量 带空心菱形的实心线,菱形指向整体

组合:new A的对象当作构造方法参数传递进来作为B类的成部变量或者A类作为B类成 员变量并已经new A(A类和B类具有相同的生命周期) 带实心菱形的实线,菱形指向整体 总结:各种关系的强弱顺序:泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖

区分:①如果B类作为了A类的成员变量(has的关系),则一般是A类与B类是关联(A类与B类平级)、聚合(A类是整体,B类是部分)、组合的关系(A类是整体,B类是部分,且A类B类有相同的生命周期,)根据上下文语意区分:

聚合(B类即便不在A类中也可以单独存在),组合(B类不在A类中就无法单独存在)。②如果B类作为了A类的局部变量(use的关系),方法的形参,或者对静态方法的调用一般是依赖关系。UML类图如下:

UML说明:苹果手机和红米手机继承了手机这个抽象类,工厂类里根据客户端传入的参数生成相应的对象,如,客户说要红米,工厂给客户一个红米手机,客户说要苹果,工厂给客户一个苹果手机。

简单工厂有三个对象:①抽象产品类:提供抽象方法供具体产品类实现 ②具体产品类:提供具体的产品 ③工厂:根据内部逻辑返回相应的产品

3.代码实现

(1)抽象产品类Phone 这里可以是类,也可以是接口或者抽象类,千万不要思维定式。我比较喜欢面向接口编程,所以我这里用了接口。

public interface Phone {
     void produce();
}

(2)具体产品类

在这里插入代码片public class ApplePhoneImpl implements Phone{
    @Override
    public void produce() {
        System.out.println("生产苹果手机");
    }
}
public class RedmiPhoneImpl implements Phone{
    @Override
    public void produce() {
        System.out.println("生产了红米手机");
    }
}

(3)工厂类

public class Factory {
    
    public Phone getPhone(String type){
        Phone phone = null;
        if("红米".equals(type)){
            phone = new RedmiPhoneImpl();
        }else if("苹果".equals(type)){
            phone = new ApplePhoneImpl();
        }//.....
        return phone;
    }
}

(4)客户端使用

@Test
    public void test1(){
        Factory factory = new Factory();
        
        Phone redmiPhone = factory.getPhone("红米");
        System.out.println(redmiPhone);
        redmiPhone.produce();

        Phone applePhone = factory.getPhone("苹果");
        System.out.println(applePhone);
        applePhone.produce();
    }

运行结果如下:

4.总结

优点:只需要传入一个正确的参数,就可以获取你所需要的对象而无需知道其创建对象的细节

缺点:扩展性差,当增加新的产品需要修改工厂类的判断逻辑,违背开闭原则,如我想要买一个华为手机的话,除了新增华为手机这个产品类,还需要修改工厂中的逻辑

5.升级版本

通过反射创建对象,以改进了之前提到的缺点(增加新的产品需要修改工厂类的判断逻辑),现在增加新的具体产品的时候不需要修改工厂中的代码。满足了开闭原则。(1)工厂类代码如下:

public class FactoryPlus {
    public Phone getPhone(Class clazz) throws Exception {
        return (Phone) Class.forName(clazz.getName()).newInstance();
    }
}

(2)客户端代码如下:

@Test
public void test2() throws Exception {
        FactoryPlus factory = new FactoryPlus();

        Phone redmiPhone = factory.getPhone(RedmiPhoneImpl.class);
        System.out.println(redmiPhone);
        redmiPhone.produce();

        Phone applePhone = factory.getPhone(ApplePhoneImpl.class);
        System.out.println(applePhone);
        applePhone.produce();
    }

运行结果如下:

(3)总结优点:工厂类中的方法逻辑,是利用反射机制生成对象返回,好处是增加一种产品时,不需要修改工厂类中的代码。满足了开闭原则。
缺点:这种写法粗看牛逼,细想之下,不谈reflection的效率还有以下问题:个人觉得不好,因为Class.forName(clz.getName()).newInstance()调用的是无参构造函数生成对象,它和new Object()是一样的性质,而工厂方法应该用于复杂对象的初始化 ,当需要调用有参的构造函数时便无能为力了,这样像为了工厂而工厂,没有实际意义。2 不同的产品需要不同额外参数的时候 不支持。

6.再升级(重要)

(1)工厂类:

public class FactoryPlusPlus {

    /**<bean id="applePhone" class="com.wander.design.simplefactory.product.ApplePhoneImpl"/>
     * 熟悉吧!!!spring ioc 就是通过将下面的这句话配置在配置文件中,再利用反射创建对象,
     * 这就是spring ioc的原理:工厂+配置文件+反射!!以达到彻底解耦的目的**/
    private static String className="com.wander.design.simplefactory.product.ApplePhoneImpl";

    public static Phone getPhone() throws Exception {
        return (Phone) Class.forName(className).newInstance();
    }
}


(2) 客户端:

@Test
    public void test3() throws Exception {
        Phone phone = FactoryPlusPlus.getPhone();
        phone.produce();
    }

(3)说明:spring ioc容器的原理就是这种方式:工厂+配置文件+反射,spring通过读取配置文件(<bean id="applePhone" class="com.wander.design.simplefactory.product.ApplePhoneImpl"/>),获取到className再利用反射机制Class.forName(className).newInstance()得到对象赋值给配置文件里bean标签的id属性的值,就是工厂生成的对象名。

优点:就是满足OCP原则,在不修改源代码的前提下切换底层的实现,达到解耦的目的!

7.开发常用版本:多方法工厂

使用以上两种方法的工厂,都有两个缺点:一是不同的产品需要不同额外参数的时候不支持。二是如果使用时传递的type、Class出错,将不能得到正确的对象,容错率不高。

而多方法的工厂模式为不同产品,提供不同的生产方法,使用时 需要哪种产品就调用该种产品的方法,使用方便、容错率高。

(1)工厂类代码如下:

public class FactoryMoreMethod {

    public static Phone getApple(){
        return new ApplePhoneImpl();
    }

    public static Phone getRedmi(){
        return new RedmiPhoneImpl();
    }

    /**新增华为手机产品,只需要在工厂中增加一个静态方法即可,不需要修改原有的方法**/
    public static Phone getHonor(){
        return new HonorPhoneImpl();
    }
}

(2)客户端代码:

@Test
    public void test3() throws Exception {
        Phone apple = FactoryMoreMethod.getApple();
        apple.produce();

        Phone redmi = FactoryMoreMethod.getRedmi();
        redmi.produce();

        Phone honor = FactoryMoreMethod.getHonor();
        honor.produce();
    }

(3)应用场景:查看java源码:java.util.concurrent.Executors类便是一个生成Executor 的工厂 ,其采用的便是 多方法静态工厂模式:例如ThreadPoolExecutor类构造方法有5个参数,其中三个参数写法固定,前两个参数可配置,如下写。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

又如JDK想增加创建ForkJoinPool类的方法了,只想配置parallelism参数,便在类里增加一个如下的方法:

public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

(4)总结:多方法工厂的优势,方便创建同种类型的复杂参数对象。

8.应用场景

(1)在任何需要生成复杂对象的地方,都可以使用工厂方法模式。直接用new可以完成的不需要用工厂模式个人理解,重点就是这个复杂 (构造函数有很多参数)和 是否可以 直接用new。

(2)客户端只知道传入工厂类的参数,对于如何创建对象并不关心。(对于升级后的简单工厂模式只知道类名即可) 

(3)工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。(对于升级后的简单工厂模式已解决这个问题,符合开闭原则) 

(4)简单工厂在源码中的使用--Calendar:

Calendar cal = Calendar.getInstance(zone.toTimeZone(), locale);
public static Calendar getInstance(TimeZone zone, Locale aLocale) {
      return createCalendar(zone, aLocale);
  }

  private static Calendar createCalendar(TimeZone zone, Locale aLocale) {
      '部分删减'
      Calendar cal = null;

      if (aLocale.hasExtensions()) {
          String caltype = aLocale.getUnicodeLocaleType("ca");
          if (caltype != null) {
              switch (caltype) {
              case "buddhist":
              cal = new BuddhistCalendar(zone, aLocale);
                  break;
              case "japanese":
                  cal = new JapaneseImperialCalendar(zone, aLocale);
                  break;
              case "Gregory":
                  cal = new GregorianCalendar(zone, aLocale);
                  break;
              }
          }
      }
      if (cal == null) {
          if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
              cal = new BuddhistCalendar(zone, aLocale);
          } else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
                     && aLocale.getCountry() == "JP") {
              cal = new JapaneseImperialCalendar(zone, aLocale);
          } else {
              cal = new GregorianCalendar(zone, aLocale);
          }
      }
      return cal;
  }

(2)工厂方法模式

1.认识

①一句话来说就是,定义一个创建对象的接口,让子类决定实例化那个类。因为当需要增加一个新的产品时,我们需要增加一个具体的产品类和与之对应的具体子工厂,然后在具体子工厂方法中进行对象实例化,所以称为工厂方法模式。

②具体来说就是定义一个用于创建对象的工厂接口,但让实现这个工厂接口的子类来决定实例化哪个具体产品类,工厂方法让类的实例化推迟到子类中进行。

③工厂方法模式非常符合“开闭原则”,当需要增加一个新的产品时,我们只需要增加一个具体的产品类和与之对应的具体工厂即可,无须修改原有系统。同时在工厂方法模式中用户只需要知道生产产品的具体工厂即可,无须关系产品的创建过程,甚至连具体的产品类名称都不需要知道。

④虽然他很好的符合了“开闭原则”,但是由于每新增一个新产品时就需要增加两个类,这样势必会导致系统的复杂度增加。

2.UML类图

UML说明:苹果手机和红米手机实现了手机这个抽象类,苹果工厂和红米工厂实现了抽象工厂,苹果工厂当然要生产(依赖)苹果手机,红米工厂当然要生产(依赖)红米。客户要买苹果手机要去问苹果工厂要苹果手机,客户要买红米手机当然要去问红米工厂要红米手机。

工厂方法有四个对象:抽象产品类:提供抽象方法供具体产品类实现 具体产品类:提供具体的产品 抽象工厂:提供抽象方法供具体工厂实现 具体工厂:提供具体的工厂

3.代码实现

(1)抽象产品类和简单工厂的抽象产品类一样

(2)具体产品类和简单工厂的具体产品类一样 

(3)抽象工厂

public interface Factory {
    Phone getPhone();
}

(4)具体工厂

public class AppleFactoryImpl implements Factory{

    @Override
    public Phone getPhone() {
        return new ApplePhoneImpl();
    }
}

public class RedmiFactoryImpl implements Factory{
    @Override
    public Phone getPhone() {
        return new RedmiPhoneImpl();
    }
}

(5)客户端

@Test
public void test1(){

        Factory applePhoneFactory = new AppleFactoryImpl();
        Factory redmiPhoneFactory = new RedmiFactoryImpl();

        Phone applePhone = applePhoneFactory.getPhone();
        Phone redmiPhone = redmiPhoneFactory.getPhone();

        System.out.println(applePhone);
        System.out.println(redmiPhone);

        applePhone.produce();
        redmiPhone.produce();
    }

执行结果如下:

4.总结

优点:①用户只需要关心所需产品的对应工厂,无需关心细节 

②完全支持开闭原则,提高可扩展性。所谓的开闭原则就是对扩展开放,对修改关闭,再说白点就是实现工厂方法以后要进行扩展时不需要修改原有代码,只需要增加一个工厂实现类和产品实现类就可以。这样的好处可以降低因为修改代码引进错误的风险。

缺点:①每加入一种产品,会创建一个具体工厂类和具体产品类,因此,类的个数容易过多,增加复杂度。

②抽象工厂和抽象产品增加了系统的抽象性和理解难度

5.工厂方法与简单工厂的区别

①可以看出,工厂方法模式特点:不仅仅做出来的产品要抽象, 工厂也应该需要抽象。

②工厂方法使一个产品类的实例化延迟到其具体工厂子类. 

③工厂方法的好处就是更拥抱变化。当需求变化,只需要增删相应的类,不需要修改已有的类。

④而简单工厂需要修改工厂类的方法,多方法静态工厂模式需要增加一个静态方法。

缺点:引入抽象工厂层后,每次新增一个具体产品类,也要同时新增一个具体工厂类,所以我更青睐多方法静态工厂,每次新增一个具体产品类,工厂只需要新增一个静态方法

6.应用场景

(1)客户端不知道它所需要的对象的类。(需要知道所需的对象的类使用升级版简单工厂模式,需要知道所需的参数的类使用简单工厂模式)。

(2)抽象工厂类通过其子类来指定创建哪个对象。

(3)简单工厂在源码中的使用--Collection:Collection(抽象工厂):

public interface Collection<E> extends Iterable<E> {
 Iterator<E> iterator();
}

ArrayList(具体工厂):

public class ArrayList<E>{
    public Iterator<E> iterator() {
        return new Itr();
    }
}

Iterator(抽象产品):

public interface Iterator<E> {
 boolean hasNext();
}

Itr(具体产品):

private class Itr implements Iterator<E> {
 int cursor; // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    public boolean hasNext() {
            return cursor != size;
    }
    '省略代码...'
}

(3)抽象工厂模式

1.认识

①一句话来说就是,创建相关或依赖对象的家族,而无需明确指定具体类。因为我们可以定义具体产品类实现不止一个抽象工厂接口,一个工厂也可以生成不止一个产品类,是三个模式中较为抽象,并具一般性的模式。我们在使用中要注意使用抽象工厂模式的条件。

②所谓抽象工厂模式就是提供一个接口,用于创建相关或者依赖对象的家族,而不需要明确指定具体类。他允许客户端使用抽象的接口来创建一组相关的产品,而不需要关心实际产出的具体产品是什么。这样一来,客户就可以从具体的产品中被解耦。它的优点是隔离了具体类的生成,使得客户端不需要知道什么被创建了,而缺点就在于新增新的行为会比较麻烦,因为当添加一个新的产品对象时,需要更改接口及其下所有子类。

2.UML类图

UML说明:具体的苹果手机产品和具体的红米手机产品实现了手机产品抽象类,具体的苹果充电器产品和具体的红米充电器产品实现了充电器产品抽象类。具体的苹果工厂和具体的红米工厂实现了手机抽象工厂,然后苹果工厂生产苹果手机和苹果充电器,红米工厂生成红米手机和红米充电器。客户想要苹果手机和苹果充电器就要向苹果工厂要产品(对象),客户想要红米手机和红米充电器就要向红米工厂要产品(对象)。

工厂方法有四个对象:

抽象产品类:为每种具体产品声明接口,如图中Phone手机抽象类和Charger充电器抽象类 

具体产品类:定义了工厂生产的具体产品对象,实现抽象产品接口声明的业务方法,如图中ApplePhoneImpl、RedmiPhoneImpl,AppleChargerImpl,RedmiChargerImpl

抽象工厂:它声明了一组用于创建一种产品的方法,每一个方法对应一种产品,如上述类图中的Factory就定义了两个方法,分别创建Phone和Charger

具体工厂:它实现了在抽象工厂中定义的创建产品的方法,生产一组具体产品,这组产品构件成了一个产品种类,每一个产品都位于某个产品等级结构中,如上述类图中的AppleFactoryImpl和RedmiFactoryImpl

3.代码实现

(1)抽象的产品

public interface Phone {
    void produce();
}

public interface Charger {
    void produce();
}

(2)具体的产品 

① 苹果具体的产品

public class AppleChargerImpl implements Charger{

    @Override
    public void produce() {
        System.out.println("生产苹果充电器");
    }
}

public class ApplePhoneImpl implements Phone {
    @Override
    public void produce() {
        System.out.println("生产苹果手机");
    }
}

② 红米具体的产品

public class RedmiChargerImpl implements Charger{
    @Override
    public void produce() {
        System.out.println("生产红米充电器");
    }
}


public class RedmiPhoneImpl implements Phone {
    @Override
    public void produce() {
        System.out.println("生产了红米手机");
    }
}

(3)抽象工厂

public interface Factory {
    Phone getPhone();

    Charger getCharger();
}

(4)具体的工厂

public class AppleFactoryImpl implements Factory {

    @Override
    public Phone getPhone() {
        return new ApplePhoneImpl();
    }

    @Override
    public Charger getCharger() {
        return new AppleChargerImpl();
    }
}

public class RedmiFactoryImpl implements Factory {
    
    @Override
    public Phone getPhone() {
        return new RedmiPhoneImpl();
    }

    @Override
    public Charger getCharger() {
        return new RedmiChargerImpl();
    }
}


(5)客户端

@Test
    public void test1(){
        Factory appleFactory = new AppleFactoryImpl();
        Phone applePhone = appleFactory.getPhone();
        Charger appleCharger = appleFactory.getCharger();
        System.out.println(appleFactory);
        applePhone.produce();
        appleCharger.produce();

        Factory redmiFactory = new RedmiFactoryImpl();
        Phone redmiPhone = redmiFactory.getPhone();
        Charger redmiCharger = redmiFactory.getCharger();
        System.out.println(redmiFactory);
        redmiPhone.produce();
        redmiCharger.produce();

    }


(5)执行结果

4.总结

优点:

①具体产品在应用层代码隔离,无须关系创建细节 

②将一个系列的产品统一到一起创建

③对于增加新的产品族(一个具体工厂就是一个产品族),抽象工厂模式很好地支持了“开闭原则”,只需要增加具体产品并对应增加一个新的具体工厂,对已有代码无须做任何修改。

缺点:①规定了所有可能被创建的产品集合,产品族扩展新的产品(工厂中添加新的方法)困难。如果产品族扩展新的产品,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。

②增加了系统的抽象性和理解难度

5.应用场景

抽象工厂在实际的开发中运用并不多,主要是在开发工程中很少会出现多个产品种类的情况,大部分情况使用以上两种工厂模式即可解决

6.个人总结

一句话总结工厂模式:方便创建 同种产品类型的 复杂参数 对象工厂模式重点就是适用于 构建同产品类型(同一个接口 基类)的不同对象时,这些对象new很复杂,需要很多的参数,而这些参数中大部分都是固定的,so,懒惰的程序员便用工厂模式封装之。(如果构建某个对象很复杂,需要很多参数,但这些参数大部分都是“不固定”的,应该使用建造者Builder模式)

(4)单例模式

1.认识

①一句话来说就是,某个类只能有一个实例,提供一个全局的访问点。

②单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。

③使用Singleton的好处还在于可以节省内存,因为它限制了实例的个数,有利于Java垃圾回收(garbage collection),而且确保所有对象都访问唯一实例。但是不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。用单例模式,就是在适用其优点的状态下使用

2.UML类图

UML说明:1.构造方法私有化:可以使得该类不被实例化即不能被new 2.在类本身里创建自己的对象 3.提供一个公共的方法供其他对象访问

3.代码实现

(1)饿汉式

①第一种:

public class Singleton {

    /**
     * static:
     * ①表示共享变量,语意符合
     * ②使得该变量能在getInstance()静态方法中使用
     * final:
     * ①final修饰的变量值不会改变即常量,语意也符合,当然不加final也是可以的
     * ②保证修饰的变量必须在类加载完成时就已经进行赋值。
     * final修饰的变量,前面一般加static
     */
    private static final Singleton singleton = new Singleton();

    /**
     * 私有化构造方法,使外部无法通过构造方法构造除singleton外的类实例
     * 从而达到单例模式控制类实例数目的目的
     */
    private Singleton(){}

    /**
     * 类实例的全局访问方法
     * 因为构造方法以及被私有化,外部不可能通过new对象来调用其中的方法
     * 加上static关键词使得外部可以通过类名直接调用该方法获取类实例
     * @return
     */
    public static Singleton getSingleton() {
        return singleton;
    }
}

②第二种

public class SingletonStatic {

    private static final SingletonStatic singletonStatic;

    /**
     * 和第一种没有什么区别,这种看起来高大上面试装逼使用
     */
    static {
        singletonStatic = new SingletonStatic();
    }

    private SingletonStatic() {}

    public static SingletonStatic getSingletonStatic(){
        return singletonStatic;
    }
}
说明:①优点:一般使用static和final修饰变量(具体作用已经在代码里描述了),只在类加载时才会初始化,以后都不会,线程绝对安全,无锁,效率高。
②缺点:类加载的时候就初始化,不管用不用,都占用空间,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)

:这里有两个小知识点:

a.如果是final非static成员,必须在构造器、代码块、或者直接定义赋值

b.如果是final static 成员变量,必须直接赋值 或者在静态代码块中赋值

(2)懒汉式
public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){}

    public static Singleton getSingleton() {
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}


说明:

①优点:在外部需要使用的时候才进行实例化,不使用的时候不会占用空间。

②缺点:线程不安全。看上去,这段代码没什么明显问题,但它不是线程安全的。假设当前有N个线程同时调用getInstance()方法,由于当前还没有对象生成,所以一部分同时都进入if语句new Singleton(),那么就会由多个线程创建多个多个user对象。

(3)线程安全的懒汉式
public class Singleton {

    private static Singleton singleton;

    private Singleton(){};

    private static synchronized Singleton getSingleton(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

说明:①优点:解决了懒汉式线程不安全的问题 ②缺点:线程阻塞,影响性能。

(4)DCL单例 - 高性能的懒汉式
public class Singleton {
 /*volatile在这里发挥的作用是:禁止指令重排序(编译器和处理器为了优化程序性能
    * 而对指令序列进行排序的一种手段。)
    * singleton = new Singleton();这句代码是非原子性操作可分为三行伪代码
    * a:memory = allocate() //分配内存,在jvm堆中分配一段区域
    * b:ctorInstanc(memory) //初始化对象,在jvm堆中的内存中实例化对象
    * c:instance = memory //赋值,设置instance指向刚分配的内存地址
    * 上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。
    * 重排序是为了优化性能,但是不管怎么重排序,在单线程下程序的执行结果不能被改变
    * 保证最终一致性。而在多线程环境下,可能发生重排序,会影响结果。
    * ①若A线程执行到代码singleton = new Singleton()时;
    * ②同时若B线程进来执行到代码到第一层检查if (singleton == null)
    * ③当cpu切换到A线程执行代码singleton = new Singleton();时发生了指令重排序,
    * 执行了a-b,没有执行c,此时的singleton对象只有地址,没有内容。然后cpu又切换到了B线程,
    * 这时singleton == null为false(==比较的是内存地址),
    * 则代码会直接执行到了return,返回一个未初始化的对象(只有地址,没有内容)。
    * */
    private volatile static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        /*第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
        * ①当多个线程第一次进入,所有线程都进入if语句
        * ②当多个线程第二次进入,因为singleton已经不为null,因此所有线程都不会进入if语句,
        * 即不会执行锁,从而也就不会因为锁而阻塞,避免锁竞争*/
        if (singleton == null) {
            /*第一层锁,保证只有一个线程进入,
            * ①多个线程第一次进入的时候,只有一个线程会进入,其他线程处于阻塞状态
            * 当进入的线程创建完对象出去之后,其他线程又会进入创建对象,所以有了第二次if检查
            * ②多个线程第二次是进入不到这里的,因为已被第一次if检查拦截*/
            synchronized (Singleton.class) {
                /*第二层检查,防止除了进入的第一个线程的其他线程重复创建对象*/
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

说明:代码注释已详细讲解volatile在该单例模式的作用,已经双重锁的作用。①优点:解决了线程阻塞的问题 ②缺点:多个线程第一次进入的时候会造成大量的线程阻塞,代码不够优雅。

(5)静态内部类的方式
public class Singleton {

    private Singleton(){}

    private static class LayzInner{
        private static Singleton singleton = new Singleton();
    }
    
    public static Singleton getSingleton(){
        return LayzInner.singleton;
    }
}

说明:①优点:第一次类创建的时候加载,避免了内存浪费,不存在阻塞问题,线程安全,唯一性 ②缺点:序列化-漏洞:反射,会破坏内部类单例模式

(6)枚举单例模式
public enum EnumSingleton {
    INSTANCE;
    private Singleton singleton;
    EnumSingleton(){
        singleton = new Singleton();
    }
    public Singleton getSingleton(){
        return singleton;
    }
}

说明:单元素的枚举类型已经成为实现Singleton的最佳方法,无法反射创建对象,但是特殊的饿汉式。

(7)静态内部类升级版

借鉴枚举单例的内部实现的方式

public class Singleton {
    private Singleton(){
        if(LayzInner.singleton != null){
            throw new RuntimeException("不能够进行反射!");
        }
    }

    private static class LayzInner{
        private static Singleton singleton = new Singleton();
    }

    public static Singleton getSingleton (){
        return LayzInner.singleton;
    }
}

说明:①优点:第一次类创建的时候加载,避免了内存浪费,不存在阻塞问题,线程安全,唯一性,解决了反射会破坏内部类单例模式的问题

②缺点:不是官方的

(8)容器式单例
public class Singleton {
    private Singleton() {
    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className) {
        synchronized (ioc) {
            if (ioc.containsKey(className)) {
                Object o = null;
                try {
                    o = Class.forName(className).newInstance();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return o;
            } else {
                return ioc.get(className);
            }
        }
    }
}


说明:Spring ioc 单例 是懒汉式 枚举上的升级

(9)ThreadLocal单例

说明:局部单例模式:某一个线程里唯一 

①ThreadLocal的作用呢,是提供线程内的局部变量,在多线程环境访问时,能保证各个线程内的ThreadLocal变量各自独立。也就是说每个线程的ThreadLocal变量是自己专用的,其他线程是访问不到的。

②ThreadLocal最常用于在多线程环境下存在对非线程安全对象的并发访问,而且该对象不需要在线程内共享,如果对该对象加锁,会造成大量线程阻塞影响程序性能,这时候就可以使用ThreadLocal来使每个线程都持有该对象的副本,这是典型的空间换取时间从而提高执行效率的方式。例如项目里经常使用的SimpleDateFormat日期格式化对象,该对象是线程不安全的,而且不需要在线程内共享,因此可以使用ThreadLocal保证其线程安全。

(5)原型模式

1.认识

①一句话来说就是,通过复制现有的实例来创建新的实例。因为是同过原有的对象创建新的对象,所以称为原型模式。

②原型模式是用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。原型模式允许一个对象再创建另外一个可定制的对象,无须知道任何创建的细节。

③用于创建重复的对象,同时又能保证性能。

(1)浅拷贝:我们只拷贝对象中的基本数据类型(8种),对于数组、容器、引用对象等都不会拷贝,只会拷贝对这些对象的引用。

(2)深拷贝:不仅能拷贝基本数据类型,还能拷贝那些数组、容器、引用对象(不仅拷贝对这些对象的引用,而且拷贝对象本身)。

2.UML类图

UML说明:实体类实现Cloneable接口,重写clone方法

3.代码实现

(1)Prototype类:

public class Prototype implements Cloneable {

    private Integer id;
    private String name;
    private Map<String, Double> map;

    @Override
    protected Prototype clone() throws CloneNotSupportedException {
        //浅拷贝方式
        Prototype prototype = (Prototype) super.clone();
        //深拷贝方式:对每一个复杂类型分别进行克隆
        //测试浅拷贝的时候注释下面代码
        prototype.map = (Map<String, Double>) ((HashMap)this.map).clone();
        return prototype;
    }

    public Prototype(Integer id, String name, Map<String, Double> map) {
        this.id = id;
        this.name = name;
        this.map = map;
    }
/**省略get、set方法和toString方法*/
}


(2)客户端:

public class Client {
    @Test
    public void test() throws CloneNotSupportedException {
        Map<String, Double> map = new HashMap<>();
        map.put("数学",100D);
        Prototype prototype = new Prototype(1,"小明",map);
        
        Prototype prototype1 = prototype.clone();
        Map<String, Double> map1 = prototype1.getMap();
        map1.put("数学",99d);
        
        System.out.println(prototype);
        System.out.println(prototype1);
    }
}


(3)执行结果:浅拷贝:

改变其中一个对象map的值,两个对象的map内容都发生了变化深拷贝:

改变其中一个对象map的值,该对象的map内容发生了变化,另一个对象map的内容没有发生变化

4.总结

优点:①提高了性能,在需要短时间创建大量的对象和创建对象很耗时的情况下,原型模式比通过new对象大大提高了时间效率。

② 逃避构造函数的约束。

缺点:

1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。

2、实现原型模式每个派生类都必须实现 Clone接口。

5.应用场景

1.通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。比如,向数据库表插入多条测试数据,可以用到。

2.在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与Java融为浑然一体,大家可以随手拿来使用。

(6)建造者模式

1.认识:

①一句话来说:封装一个复杂对象的构建过程,并可以按步骤构造。因为需要对对象一步步建造起来,所以称为建造者模式。

②将复杂产品的构建过程封装分解在不同的方法中,使得创建过程非常清晰,能够让我们更加精确的控制复杂产品对象的创建过程,同时它隔离了复杂产品对象的创建和使用,使得相同的创建过程能够创建不同的产品。但是若内部变化复杂,会有很多的建造类。

2.UML类图:

UML说明:Product(产品角色):一个具体的产品对象。Builder(抽象建造者):创建一个Product对象的各个部件指定的抽象接口。ConcreteBuilder(具体建造者):实现抽象接口,构建和装配各个部件。Director(指挥者):构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象。它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。

3.代码如下:

1.产品类:

public class Product {
    private String part1;//可以是任意类型
    private String part2;
    private String part3;
 /**set get 方法省略
}

2.抽象建造者

public abstract class Builder{

    Product product = new Product();
    public abstract void buildPart1();
    public abstract void buildPart2();
    public abstract void buildPart3();
    public Product getResult(){
        return product;
    };
}


3.具体建造者

public class ConcreteBuilder extends Builder {

    @Override
    public void buildPart1() {
        System.out.println("建造part1");
    }

    @Override
    public void buildPart2() {
        System.out.println("建造part2");
    }

    @Override
    public void buildPart3() {
        System.out.println("建造part3");
    }
}


4.指挥者:

public class Director {

    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public Product build(){
        builder.buildPart1();
        builder.buildPart2();
        builder.buildPart3();
        return builder.getResult();
    }
}


5.客户端

public class Client {

    @Test
    public void test() {
        Builder builder = new ConcreteBuilder();
        Director director = new Director(builder);
        director.build();
    }

}

6.执行结果

4.总结

优点:1、建造者独立,易扩展。将复杂产品的构建过程封装分解在不同的方法中,使得创建过程非常清晰,能够让我们更加精确的控制复杂产品对象的创建过程。

2、便于控制细节风险。它隔离了复杂产品对象的创建和使用,使得相同的创建过程能够创建不同的产品。

缺点:1、产品必须有共同点,范围有限制。

2、如内部变化复杂,会有很多的建造类,导致系统庞大。

应用场景1、需要生成的对象具有复杂的内部结构。2、需要生成的对象内部属性本身相互依赖。

5.应用场景

JAVA 中的 StringBuilder。

六、个人体会

设计模式是一种解决问题的思维和方式,不要生搬硬套,为了设计模式而模式。本文作者:王德印,欢迎复制下方链接关注博主动态。

链接:https://blog.csdn.net/qq_41889508/article/details/105953114
如果看到这里,说明你喜欢这篇文章,请 转发、点赞。微信搜索「web_resource」,关注后回复「进群」或者扫描下方二维码即可进入无广告交流群。
↓扫描二维码进群↓


推荐阅读

1. GitHub 上有什么好玩的项目?

2. Linux 运维必备 150 个命令汇总

3. SpringSecurity + JWT 实现单点登录

4. 100 道 Linux 常见面试题

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存