java 设计模式学习

设计模式

1. 单例模式

1.1 场景

许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。比如我们常用的全局缓存对象。

实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

1.2 八种方式

1.2.1 饿汉式

饿汉式是指全局的单例实例在类装载时就构建。无论这个实例用不用都存在在内存中。

public class SingletonNoLazy {

    private static final SingletonNoLazy INSTANCE = new SingletonNoLazy();
    
    /**
     * 静态代码块
     */
    /*static {
        INSTANCE = new Singleton();
    }*/

    // Private constructor
    // default public constructor
    private SingletonNoLazy() {};

    public static SingletonNoLazy getInstance() {
        return INSTANCE;
    }
}

优点:写法简单,在类装载时完成实例化。避免线程同步问题。

缺点:类加载时就实例化,如果类对象没有使用则会造成内存浪费问题。

静态变量和静态代码块装载顺序:

Java中的静态变量和静态代码块是在类加载的时候就执行的,实例化对象时,先声明并实例化变量再执行构造函数。如果子类继承父类,则先执行父类的静态变量和静态代码块,再执行子类的静态变量和静态代码块。同样,接着在执行父类和子类非静态代码块和构造函数。

注意:(静态)变量和(静态)代码块的也是有执行顺序的,与代码书写的顺序一致。在(静态)代码块中可以使用(静态)变量,但是被使用的(静态)变量必须在(静态)代码块前面声明。

1.2.2 懒汉式

懒汉式是指 全局的单例实例在类装载时构建。

1.2.2.1 不安全的懒汉式
public class SingletonLazyNoSafe {

    private static SingletonLazyNoSafe INSTANCE;

    private SingletonLazyNoSafe() {}

    public static SingletonLazyNoSafe getInstance() {
        if(INSTANCE == null) {
            INSTANCE = new SingletonLazyNoSafe();
        }
        return INSTANCE;
    }
}

优点:懒加载,不浪费内存。

缺点:多线程时不是单实例。有错误,实际中不能使用。

1.2.2.2 安全的懒汉式
  1. 方法锁

    public class SingletonLazySafe {
    
        private static volatile SingletonLazySafe INSTANCE;
    
        private SingletonLazySafe() {}
    
        public synchronized static SingletonLazySafe getInstance() {
            if(INSTANCE == null) {
                   INSTANCE = new SingletonLazySafe();
                }
            }
            return INSTANCE;
        }
    }
    

    优点:可以使用,无错误。

    缺点: 方法加syn锁,但是多个线程同时获取,会阻塞,效率太低。


  2. 双重锁

    public class SingletonLazySafe {
    
     private static volatile SingletonLazySafe INSTANCE;
    
     private SingletonLazySafe() {}
    
    /**
      * 1. 方法加syn锁,但是多个线程同时获取,会阻塞,效率太低
      * 2. 双重锁模式
      */
     public static SingletonLazySafe getInstance() {
         if(INSTANCE == null) {
             synchronized (SingletonLazySafe.class) {
                 if(INSTANCE == null) {
                     INSTANCE = new SingletonLazySafe();
                 }
             }
         }
         return INSTANCE;
     }
    }
    

    双重锁需要注意: 静态变量需要加volatile关键字,阻止虚拟机编译时的重排序。

    volatile 有两个作用:

    • 线程间的可见性
    • 防止指令重排

    这里的作用是防止指令重排。

    每一个不同的实例化对象对应着需要jvm中的一块内存去存放。new对象分为三步:

    • 分配内存空间
    • 初始化对象
    • 将对象指向刚分配的内存空间(返回地址值,则放开锁)

    jvm在指令优化时,会出现步骤2和3对调的情况,比如线程1在经过俩层为 null 判断后,进入 new 的动作,在还没有初始化对象时,就返回了地址值,线程2在第一个为 null 判断时,因为对象已经不为空,那么就直接返回了对象。然而当线程2打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。


  3. 静态内部类

    public class SingletonLazySafe2 {
     
     private SingletonLazyNoSafe() {}
    
     static class SingletonHolder {
         private static final SingletonLazySafe2 INSTANCE = new SingletonLazySafe2();
     }
    
     public static SingletonLazySafe2 getInstance() {
         return SingletonHolder.INSTANCE;
     }
    

    结论:加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。


  4. 枚举类

    public class SingletonByEnum {
    
           private SingletonByEnum() {}
           
           private enum SingletonEnum {
           
               /**
                * 实例
                */
               INSTANCE;
               private final SingletonByEnum instance;
               private SingletonEnum() {
                   instance = new SingletonByEnum();
               }
           
               private SingletonByEnum getInstance() {
                   return instance;
               }
           }
           
           public static SingletonByEnum getInstance() {
               return SingletonEnum.INSTANCE.getInstance();
           }
    
       }
    

    优点:除枚举方式外, 其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象.

    S: 所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例。

    private SingletonObject1(){
        if (instance != null){
            throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
        }
    }
    

2. 工厂模式

引用

以下三种工厂模式在设计模式的分类中都属于创建型模式,三种模式从上到下逐步抽象。

工厂模式是创建型模式中比较重要的。工厂模式的主要功能就是帮助我们实例化对象。之所以名字中包含工厂模式四个字,是因为对象的实例化过程是通过工厂实现的,是用工厂代替 new 操作的。

工厂模式优点:

  • 可以使代码结构清晰,有效地封装变化。在编程中,产品类的实例化有时候是比较复杂和多变的,通过工厂模式,将产品的实例化封装起来,使得调用者根本无需关心产品的实例化过程,只需依赖工厂即可得到自己想要的产品。
  • 对调用者屏蔽具体的产品类。如果使用工厂模式,调用者只关心产品的接口就可以了,至于具体的实现,调用者根本无需关心。即使变更了具体的实现,对调用者来说没有任何影响。
  • 降低耦合度。产品类的实例化通常来说是很复杂的,它需要依赖很多的类,而这些类对于调用者来说根本无需知道,如果使用了工厂方法,我们需要做的仅仅是实例化好产品类,然后交给调用者使用。对调用者来说,产品所依赖的类都是透明的。、

适用场景:

不管是简单工厂模式,工厂方法模式还是抽象工厂模式,他们具有类似的特性,所以他们的适用场景也是类似的。

首先,作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。

其次,工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。假如调用者自己组装产品需要增加依赖关系时,可以考虑使用工厂模式。将会大大降低对象之间的耦合度。

再次,由于工厂模式是依靠抽象架构的,它把实例化产品的任务交由实现类完成,扩展性比较好。也就是说,当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。


2.1 简单工厂模式

简单工厂模式,真是因为简单才被叫做简单工厂模式的。

简单工厂模式包含 3 个角色(要素):

  • Factory:即工厂类, 简单工厂模式的核心部分,负责实现创建所有产品的内部逻辑;工厂类可以被外界直接调用,创建所需对象
  • Product:抽象类产品, 它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象
  • ConcreteProduct:具体产品, 它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。它要实现抽象产品中声明的抽象方法

存在的问题:

当我们需要增加一种计算时,例如开平方。这个时候我们需要先定义一个类继承 Operation 类,其中实现开平方的代码。除此之外我们还要修改 OperationFactory 类的代码,增加一个 case。这显然是违背开闭原则的。可想而知对于新产品的加入,工厂类是很被动的。我们举的例子是最简单的情况。而在实际应用中,很可能产品是一个多层次的树状结构。 简单工厂可能就不太适用了。

总结:

工厂类是整个简单工厂模式的关键。包含了必要的逻辑判断,根据外界给定的信息,决定究竟应该创建哪个具体类的对象。通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的。明确了各自的职责和权利,有利于整个软件体系结构的优化。

但是由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了

当系统中的具体产品类不断增多时候,可能会出现要求工厂类根据不同条件创建不同实例的需求.这种对条件的判断和对具体产品类型的判断交错在一起,很难避免模块功能的蔓延,对系统的维护和扩展非常不利;

为了解决这些缺点,就有了工厂方法模式。


2.2 工厂方法模式

总结:

工厂方法模式是简单工厂模式的进一步抽象和推广。

由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。

在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UxJyGGAa-1639922341416)(typro-images/image-20211213214550992.png)]

优点:

  • 一个调用者想创建一个对象,只要知道其名称就可以了。
  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
  • 屏蔽产品的具体实现,调用者只关心产品的接口。

缺点:

每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。

2.3 抽象工厂模式

工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ev4mY0yx-1639922341418)(typro-images/image-20211213214514033.png)]

3. 模板方法模式

3.1 是什么

模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。

模板方法模式是基于”继承“的;

3.2 解决了什么

  • 提高代码复用性:将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中
  • 实现了反向控制:通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制并且符合“开闭原则”

3.3 UML类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vyzhxg97-1639922341418)(typro-images/image-20211215134543307.png)]

3.4 例子

UML图是一个导出csv格式数据的一个模板方法模式的应用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A38U3Fo9-1639922341419)(typro-images/image-20211215134834072.png)]

4. 建造者模式

4.1 是什么

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

场景: 当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数,考虑使用建造者模式。

4.2 解决了什么

当一个类的构造函数参数超过4个,而且这些参数有些是可选的时,我们通常有两种办法来构建它的对象。 例如我们现在有如下一个类计算机类Computer,其中cpu与ram是必填参数,而其他3个是可选参数,那么我们如何构造这个类的实例呢,通常有两种常用的方式:

  • 折叠构造函数模式(telescoping constructor pattern )
public class Computer {
    private String cpu;//必须
    private String ram;//必须
    private int usbCount;//可选
    private String keyboard;//可选
    private String display;//可选
}
public class Computer {
     ...
    public Computer(String cpu, String ram) {
        this(cpu, ram, 0);
    }
    public Computer(String cpu, String ram, int usbCount) {
        this(cpu, ram, usbCount, "罗技键盘");
    }
    public Computer(String cpu, String ram, int usbCount, String keyboard) {
        this(cpu, ram, usbCount, keyboard, "三星显示器");
    }
    public Computer(String cpu, String ram, int usbCount, String keyboard, String display) {
        this.cpu = cpu;
        this.ram = ram;
        this.usbCount = usbCount;
        this.keyboard = keyboard;
        this.display = display;
    }
}
  • Javabean 模式

那么这两种方式有什么弊端呢?

第一种主要是使用及阅读不方便。你可以想象一下,当你要调用一个类的构造函数时,你首先要决定使用哪一个,然后里面又是一堆参数,如果这些参数的类型很多又都一样,你还要搞清楚这些参数的含义,很容易就传混了。。。那酸爽谁用谁知道。

第二种方式在构建过程中对象的状态容易发生变化,造成错误。因为那个类中的属性是分步设置的,所以就容易出错。

为了解决这两个痛点,builder模式就横空出世了。

public class ComputerByBuilder {

    /**
     * 必须
     */
    private String cpu;
    private String ram;
    /**
     * 可选
     */
    private Integer usbCount;
    private String keyboard;
    private String display;

    private ComputerByBuilder(Builder builder) {
        this.cpu = builder.cpu;
        this.ram = builder.ram;
        this.usbCount = builder.usbCount;
        this.keyboard = builder.keyboard;
        this.display = builder.display;
    }

    public static class Builder {

        /**
         * 必须
         */
        private String cpu;
        private String ram;
        /**
         * 可选
         */
        private Integer usbCount;
        private String keyboard;
        private String display;

        public Builder(String cup,String ram) {
            this.cpu=cup;
            this.ram=ram;
        }

        public Builder setKeyBoard(String keyBoard) {
            this.keyboard = keyBoard;
            return this;
        }

        public Builder setUsbCount(int usbCount) {
            this.usbCount = usbCount;
            return this;
        }
        public Builder setKeyboard(String keyboard) {
            this.keyboard = keyboard;
            return this;
        }
        public Builder setDisplay(String display) {
            this.display = display;
            return this;
        }
        public ComputerByBuilder build(){
            return new ComputerByBuilder(this);
        }
    }

}

// Computer中定义私有参数构造方法,定义公共内部静态类builder,builder构造公共构造函数,带着必要的参数,不必要的通过set方法
ComputerByBuilder builder = new ComputerByBuilder.Builder("intel", "三星980").setDisplay("创维").build();

5. 适配器模式

6. 观察者模式

6.1 是什么

观察者模式是对象的行为模式,又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

在Java中,Observer为观察者,以接口的形式存在,通过特定的观察者实现;Observable是被观察者也叫可观察者,以类的形式存在,通过特定的主题继承。

6.2 结构

  • Observable:可观察者对象也称被观察者对象。通过把所有观察者对象的引用保存在一个聚集(源码是一个数组)里,实现对观察者对象的通知。继承了Observable的子类称为Subject。
  • 具体主题(ConcreteSubject)角色:继承了Observable,通过继承的方式可以使用父类的增加和删除观察者对象的方法。在具体主题的内部状态改变时,设置改变并给所有登记过的观察者发出通知。
  • Observer:观察者对象。为所有的具体观察者定义一个接口,在得到主题的通知时更新自己,这个接口叫做更新接口。
  • 具体观察者(ConcreteObserver)角色:存储与主题的状态自恰的状态。如果需要,具体观察者角色可以保持一个指向具体主题对象的引用。

6.3 源码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IfaQ4crC-1639922341420)(typro-images/image-20211219214803282.png)]

  1. MsgSubject作为继承了Observable的子类,充当了被观察对象,也就是主题。父类中实现了对观察者对象的一些管理。需要注意的是在notifyObservers()之前需要调用setChanged()方法改变被观察对象的状态,观察者对象才能update()。
  2. UserObserver作为实现了Observer的实现类,充当具体的观察者对象。只有一个方法update(),在被观察者对象notify后执行update()方法。

6.4 例子

/**
 * 具体主题
 */
public class MsgSubject extends Observable {

    public void change(String msg) {
        //对象状态已被改变
        this.setChanged();
        this.notifyObservers(msg);
    }
}

/**
 * 具体观察者
 */
public class UserObserver implements Observer {

    @Override
    public void update(Observable o, Object arg) {
        System.out.println("对象 "+ o +" 收到的消息为:"+ arg);
    }
}

//测试
@Test
void tt8() {
    MsgSubject msgSubject = new MsgSubject();
    Observer userObserver = new UserObserver();
    Observer userObserver2 = new UserObserver();
    Observer userObserver3 = new UserObserver();
    msgSubject.addObserver(userObserver);
    msgSubject.addObserver(userObserver2);
    msgSubject.addObserver(userObserver3);
    msgSubject.change("第er个系统消息!");
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值