设计模式面试指南

设计模式面试指南

设计模式的作用

  • 提高代码的复用率,降低开发成本和周期
  • 提高代码的可维护性和可扩展性
  • 使代码更加优雅,更容易被他人理解

设计原则

  1. 单一职责原则:一个类应该只有这一个引起他变化的原因
  2. 开闭原则:一个实体类,函数,模块,应该对外扩展开放,对内修改关闭
  3. 里氏代换原则:子类必须替换父类型
  4. 依赖倒置原则:细节应该依赖于抽象,抽象不应该依赖于细节
  5. 接口隔离原则:使用多个专门功能的接口,而不是使用单一的总接口
  6. 合成复用原则:在一个新的对象里面使用一些已用的对象,使之成为新对象的一部分
  7. 最少知识原则(迪米特法则):一个模块应该尽量少的与其他的实体之间发生相互作用,使得系统相互独立,这样当一个模块修改时,影响的模块越少,扩展起来更加容易

在这里插入图片描述

常用设计模式

  • 创建型:单例模式、工厂方法模式(及 变式)、建造者模式;
  • 结构型:适配器模式、代理模式、门面(外观)模式;
  • 行为型:策略模式、观察者模式

创建型

面试中常问的:单例(手写),简单工厂,工厂方法,抽象工厂,建造者(手写)
在这里插入图片描述

单例
  • 模式说明:实现1个类只有1个实例化对象 & 提供一个全局访问点
  • 优点:

提供一个全局访问点
对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能;节约资源

  • 缺点:

单例类的职责过重,里面的代码可能会过于复杂,在一定程度上违背了“单一职责原则”。
如果实例化的对象长时间不被利用,会被系统认为是垃圾而被回收,这将导致对象状态的丢失。
会有内存泄漏和内存溢出的风险

1·饿汉式:初始化单例时就创建

 public class Singleton {
//1. 创建私有变量 ourInstance(用以记录 Singleton 的唯一实例)
//2. 内部进行实例化
    private static Singleton ourInstance  = new  Singleton();

//3. 把类的构造方法私有化,不让外部调用构造方法实例化
    private Singleton() {
    }
//4. 定义公有方法提供该类的全局唯一访问点
//5. 外部通过调用getInstance()方法来返回唯一的实例
    public static  Singleton newInstance() {
        return ourInstance;
    }
}

枚举:既可以避免多线程同步问题;还可以防止反射和序列化带来的重新创建新对象
因为Java虚拟机会保证枚举对象的唯一性,因此每一个枚举类型和定义的枚举变量在JVM中都是唯一的。

public enum Singleton{

    //定义1个枚举的元素,即为单例类的1个实例
    INSTANCE;

    // 隐藏了1个空的、私有的 构造方法
    // private Singleton () {}
    
    public void method(){
        System.out.println("我是一个单例");
    }

}

例子:

public class User {
    //私有化构造函数
    private User(){ }
 
    //定义一个静态枚举类
    static enum SingletonEnum{
        //创建一个枚举对象,该对象天生为单例
        INSTANCE;
        private User user;
        //私有化枚举的构造函数
        private SingletonEnum(){
            user=new User();
        }
        public User getInstnce(){
            return user;
        }
    }
 
    //对外暴露一个获取User对象的静态方法
    public static User getInstance(){
        return SingletonEnum.INSTANCE.getInstnce();
    }
}

public class Test {
    public static void main(String [] args){
        System.out.println(User.getInstance());
        System.out.println(User.getInstance());
        System.out.println(User.getInstance()==User.getInstance());
    }
}
结果为true

3·懒汉式:基础实现的懒汉式是线程不安全的

class Singleton {
    // 1. 类加载时,先不自动创建单例
   //  即,将单例的引用先赋值为 Null
    private static  Singleton ourInstance  = null// 2. 构造函数 设置为 私有权限
    // 原因:禁止他人创建实例 
    private Singleton() {
    }
    
    // 3. 需要时才手动调用 newInstance() 创建 单例   
    public static  Singleton newInstance() {
    // 先判断单例是否为空,以避免重复创建
    if( ourInstance == null){
        ourInstance = new Singleton();
        }
        return ourInstance;
    }
}

4·同步锁
每次访问都要进行线程同步(即 调用synchronized锁),造成过多的同步开销(加锁 = 耗时、耗能)

// 写法1
class Singleton {
    // 1. 类加载时,先不自动创建单例
    //  即,将单例的引用先赋值为 Null
    private static  Singleton ourInstance  = null// 2. 构造函数 设置为 私有权限
    // 原因:禁止他人创建实例 
    private Singleton() {
    }
    
// 3. 加入同步锁
public static synchronized Singleton getInstance(){
        // 先判断单例是否为空,以避免重复创建
        if ( ourInstance == null )
            ourInstance = new Singleton();
        return ourInstance;
    }
}


// 写法2
// 该写法的作用与上述写法作用相同,只是写法有所区别
class Singleton{ 

    private static Singleton instance = null;

    private Singleton(){
}

    public static Singleton getInstance(){
        // 加入同步锁
        synchronized(Singleton.class) {
            if (instance == null)
                instance = new Singleton();
        }
        return instance;
    }
}

5·双重检验锁

class Singleton {
    private static  Singleton ourInstance  = nullprivate Singleton() {
    }
    
    public static  Singleton newInstance() {
     // 加入双重校验锁
    // 校验锁1:第1个if
    if( ourInstance == null){  // ①
     synchronized (Singleton.class){ // ②
      // 校验锁2:第2个 if
      if( ourInstance == null){
          ourInstance = new Singleton();
          }
      }
  }
        return ourInstance;
   }
}

// 说明
// 校验锁1:第1个if
// 作用:若单例已创建,则直接返回已创建的单例,无需再执行加锁操作
// 即直接跳到执行 return ourInstance

// 校验锁2:第2个 if 
// 作用:防止多次创建单例问题
// 原理
  // 1. 线程A调用newInstance(),当运行到②位置时,此时线程B也调用了newInstance()
  // 2. 因线程A并没有执行instance = new Singleton();,此时instance仍为空,因此线程B能突破第1层 if 判断,运行到①位置等待synchronized中的A线程执行完毕
  // 3. 当线程A释放同步锁时,单例已创建,即instance已非空
  // 4. 此时线程B 从①开始执行到位置②。此时第2层 if 判断 = 为空(单例已创建),因此也不会创建多余的实例
volatile 关键词作用:
正确的双重检查锁定模式需要需要使用 volatilevolatile主要包含两个功能。

保证可见性。使用 volatile 定义的变量,将会保证对所有线程的可见性。
禁止指令重排序优化。

由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。

6·静态内部类:根据 静态内部类 的特性,同时解决了按需加载、线程安全的问题,同时实现简洁

class Singleton {
    
    // 1. 创建静态内部类
    private static class Singleton2 {
       // 在静态内部类里创建单例
      private static  Singleton ourInstance  = new Singleton()}

    // 私有构造函数
    private Singleton() {
    }
    
    // 延迟加载、按需创建
    public static  Singleton newInstance() {
        return Singleton2.ourInstance;
    }

}

// 调用过程说明:
      // 1. 外部调用类的newInstance() 
      // 2. 自动调用Singleton2.ourInstance
       // 2.1 此时单例类Singleton2得到初始化
       // 2.2 而该类在装载 & 被初始化时,会初始化它的静态域,从而创建单例;
       // 2.3 由于是静态域,因此只会JVM只会加载1遍,Java虚拟机保证了线程安全性
      // 3. 最终只创建1个单例
简单工厂
  • 简单工厂又叫静态方法模式
  • 将“类实例化的操作”与“使用对象的操作”分开,让使用者不用知道具体参数就可以实例化出所需要的“产品”类,从而避免了在客户端代码中显式指定,实现了解耦。

模式组成
在这里插入图片描述

组成(角色)关系作用
抽象产品(Product)具体产品的父类描述产品的公共接口
具体产品(Concrete Product)抽象产品的子类;工厂类创建的目标类描述生产的具体产品
工厂(Creator)被外界调用根据传入不同参数从而创建不同具体产品类的实例

优点

  • 将“类实例化的操作”与“使用对象的操作”分开,使用者不必关心对象的创建,实现解偶
  • 把初始化实例时的工作放到工厂里进行,使代码更容易维护

缺点

  • 工厂类集中了所有实例(产品)的创建逻辑,一旦这个工厂不能正常工作,整个系统都会受到影响;
  • 违背“开放 - 关闭原则”,一旦添加新产品就不得不修改工厂类的逻辑,这样就会造成工厂逻辑过于复杂。
  • 简单工厂模式由于使用了静态工厂方法,静态方法不能被继承和重写,会造成工厂角色无法形成基于继承的等级结构。

应用场景

  • 当工厂类负责创建的对象(具体产品)比较少时
工厂方法

定义:工厂方法模式,又称工厂模式,多态工厂模式和虚拟构造器模式,通过定义工厂父类负责定义创建对象的公共接口,而子类则负责生成具体的对象。
将类的实例化(具体产品的创建)延迟到工厂类的子类(具体工厂)中完成,即由子类来决定应该实例化(创建)哪一个类。
解决的问题:
克服简单工厂模式的缺点,遵循了“开闭原则”,
在这里插入图片描述

组成(角色)关系作用
抽象产品(Product)具体产品的父类描述具体产品的公共接口
具体产品(Concrete Product)抽象产品的子类;工厂类创建的目标类描述生产的具体产品
抽象工厂(Creator)具体工厂的父类描述具体工厂的公共接口
具体工厂(Concrete Creator)抽象工厂的子类;被外界调用描述具体工厂;实现FactoryMethod工厂方法创建产品的实例

优点

  • 更符合开闭原则:新增一种产品,只需增加相应的具体产品类和相应的工厂子类即可。
  • 符合单一职责原则:每个具体的工厂类只负责创建对应的产品
  • 不使用静态工厂方法,可以形成基于继承的等级结构。

总结:工厂模式可以说是简单工厂模式的进一步抽象和拓展,在保留了简单工厂的封装优点的同时,让扩展变得简单,让继承变得可行,增加了多态性的体现。
缺点
有更多的类需要编译和运行,会给系统带来一些额外的开销;

抽象工厂

抽象工厂模式,即Abstract Factory Pattern,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类;具体的工厂负责实现具体的产品实例。一个工厂类生产一系列产品
允许使用抽象的接口来创建一组相关产品,而不需要知道或关心实际生产出的具体产品是什么,这样就可以从具体产品中被解耦。
UML图
在这里插入图片描述
优点:

  • 降低解耦
  • 增加产品族(工厂类)符合开闭原则,增加产品类,不符合开闭原则
  • 符合单一职责原则
建造者

隐藏创建对象的建造过程 & 细节,使得用户在不知对象的建造过程 & 细节的情况下,就可直接创建复杂的对象
UML:
在这里插入图片描述
优点

  • 将产品本身和产品创建过程进行解耦,可以使用相同的创建过程来得到不同的产品,也就是说细节依赖抽象
  • 更加精确控制对象的创建
  • 易于扩展符合开闭原则

缺点

  • 建造者模式之间所创建的产品都会有很多相似之处,如果产品的差异性很大,则不适合使用建造者模式
  • 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。

行为型

面试中常问的行为型设计模式:策略模式、观察者模式 和模板方法模式。
在这里插入图片描述

观察者模式

定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
观察者模式结构中通常包括观察目标和观察者两个继承层次结构:
在这里插入图片描述

  • Subject(目标):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。
  • ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。
  • Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。
  • ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中删除。

观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系统。观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布-订阅(Publish-Subscribe)。观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。
根据上面的UML实现观察者模式

public interface Observer {
    public void update(Subject subject);
}
import java.util.ArrayList;

public abstract class Subject {
    public String subjectName;

    public Subject(String subjectName) {
        this.subjectName = subjectName;
    }

    //定义一个观察者集合用于存储所有观察者对象
    protected ArrayList<Observer> observers = new ArrayList<Observer>();

    //注册方法,用于向观察者集合中增加一个观察者
    public void attach(Observer observer) {
        observers.add(observer);
        System.out.println(subjectName+"添加"+((ConcreteObserver)observer).observerName);
    }

    //注销方法,用于在观察者集合中删除一个观察者
    public void detach(Observer observer) {
        observers.remove(observer);
        System.out.println(subjectName+"移除"+((ConcreteObserver)observer).observerName);
    }

    // 声明抽象通知方法
    public abstract void notified();
}
public class ConcreteObserver implements Observer{
    public String observerName;

    public ConcreteObserver(String observerName) {
        this.observerName = observerName;
    }

    // 具体的响应方法
    @Override
    public void update(Subject subject) {
        String subjectName = ((ConcreteSubject) subject).subjectName;
        System.out.println(observerName+"收到来自"+subjectName+"的通知");
    }
}
import java.util.ArrayList;

public class ConcreteSubject extends Subject{
    public ConcreteSubject(String subjectName) {
        super(subjectName);
    }
    @Override
    public void notified() {

        //遍历观察者集合,调用每一个观察者的响应方法
        for (Object obs:observers){
            System.out.println(subjectName+"通知"+((ConcreteObserver)obs).observerName);
            ((Observer)obs).update(this);

        }
    }
}
public class Test {
    public static void main(String[] args) {
        ConcreteSubject concreteSubject = new ConcreteSubject("被观察者");
        ConcreteObserver observer = new ConcreteObserver("观察者一");
        ConcreteObserver observer2 = new ConcreteObserver("观察者二");
        concreteSubject.attach(observer);
        concreteSubject.attach(observer2);
        concreteSubject.notified();
    }
}

在这里插入图片描述

JDK对观察者模式的支持

在JDK的java.util包中,提供了Observable类以及Observer接口,它们构成了JDK对观察者模式的支持
在这里插入图片描述
Observer接口充当抽象观察者 当观察目标的状态发生变化时,该方法将会被调用,在Observer的子类中将实现update()方法,即具体观察者可以根据需要具有不同的更新行为。当调用观察目标类Observable notifyObservers()方法时,将执行观察者类中的update()方法。
Observable类充当观察者目标类
在这里插入图片描述
我们可以直接使用Observer接口和Observable类来作为观察者模式的抽象层,再自定义具体观察者类和具体观察目标类,通过使用JDK中的Observer接口和Observable类,可以更加方便地在Java语言中应用观察者模式。
推荐阅读:Java设计模式之观察者模式

策略模式

定义一系列算法,将每个算法封装到具有公共接口的一系列策略类中,从而使它们可以相互替换 & 让算法可在不影响客户端的情况下发生变化

  • 将算法的责任和本身解耦,使得算法可独立于使用外部而变化
  • 客户端方便根据外部条件选择不同的策略来解决不同的问题

策略模式仅仅封装算法(包括添加 & 删除),但策略模式并不决定在何时使用何种算法,算法的选择由客户端来决定在这里插入图片描述
优点

  • 策略类之间可以自由切换
  • 易于扩展,新增策略类直接实现策略接口,即可,符合开闭原则
  • 避免使用多重ifelse

缺点

  • 客户端必须知道所有的策略类
  • 策略模式将产生很多的策略类,增加了类的数量
模板方法模式

定义一个模板结构,将具体内容延迟到子类里面去实现,在不改变模板结构的情况下,在子类中重新定义模板的内容,模板方法是基于继承的
解决问题:

  • 提高代码的复用性
    将相同部分放到抽象的父类中,而将不同的代码放入不同的子类中

在这里插入图片描述
优点

  • 提高代码的复用性,将相同的代码放到了父类中
  • 提高了扩展性,将不同的代码放到不同的子类中,通过对子类增加新的行为
  • 符合开闭原则

缺点
引入了抽象类,每一个不同的实现都需要一个子类来实现,导致类的个数增加,从而增加了系统实现的复杂度。

结构型

面试中常问的是:适配器模式,代理模式,外观模式
在这里插入图片描述

适配器模式

定义一个包装类,用于包装不兼容的接口对象,包装类=适配器Adapter,被包装的对象=适配者Adaptee=被适配的类
主要作用:把一个类的接口变换成客户端所期待的另一种接口,从而使原本借口不匹配而无法一起工作的两个类能够在一起工作,适配器分为:类适配器&对象适配器

类适配器

类的适配器模式是把适配的类的API转换成为目标类的API。
UML类
在这里插入图片描述
为使Target能够使用Adaptee类里的SpecificRequest方法,故提供一个中间环节Adapter类(继承Adaptee & 实现Target接口),把Adaptee的API与Target的API衔接起来(适配)。

对象适配器

与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用委派关系连接到Adaptee类。
在这里插入图片描述
为使Target能够使用Adaptee类里的SpecificRequest方法,故提供一个中间环节Adapter类(包装了一个Adaptee的实例),把Adaptee的API与Target的API衔接起来(适配)。
优点

  • 更好的复用性
  • 透明简单
  • 更好的扩展
  • 解耦性,符合开闭原则

缺点

  • 过多的使用适配器模式,会使系统特别凌乱,不利于整体的把握
  • 类适配器,使用继承的方式,导致高耦合,低内聚
  • 对象适配器,采用组合的方式,使得低耦合,高内聚
外观模式

定义了一个高层、统一的接口,外部与通过这个统一的接口对子系统中的一群接口进行访问。

通过创建一个统一的外观类,用来包装子系统中一个 / 多个复杂的类,客户端可通过调用外观类的方法来调用内部子系统中所有方法

在这里插入图片描述
主要作用

  • 实现客户类与子系统的松耦合
  • 降低原有的系统复杂度
  • 提高了客户端使用的便捷性,使得客户端无需关心子系统的工作细节,通过外观角色即可调用

在这里插入图片描述
优点

  • 降低了客户类与子系统的耦合度,实现了子系统与客户之间的松耦合
  • 外观模式对客户屏蔽了子系统组件,从而简化了接口,减少了客户处理的对象数目并使子系统的使用更加简单。
  • 降低原有系统的复杂度和系统中的编译依赖性,并简化了系统在不同平台之间的移植过程

缺点

  • 增加新的子系统就会修改外观类或者客户端源码,违背了开闭原则
  • 不能很好的限制客户端使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性。

适配器模式是将一个对象包装起来以改变其接口,而外观是将一群对象 ”包装“起来以简化其接口。它们的意图是不一样的,适配器是将接口转换为不同接口,而外观模式是提供一个统一的接口来简化接口。

静态代理

给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用,通过引入代理对象的方式来间接访问目标对象,防止直接访问目标对象给系统带来不必要的复杂性
在这里插入图片描述

优点

  • 协调调用者和被调用者,降低了系统的耦合度
  • 代理对象作为客户端和目标对象之间中介,起到了保护目标对象的作用

缺点

  • 由于在客户端和真实主题之间增加了代理对象,因此会造成请求的处理速度变慢;
  • 实现代理模式需要额外的工作(有些代理模式的实现非常复杂),从而增加了系统实现的复杂度。

应用场景
在这里插入图片描述

动态代理

设计动态代理类(DynamicProxy)时,不需要显式实现与目标对象类(RealSubject)相同的接口,而是将这种实现推迟到程序运行时由 JVM来实现

  1. 在使用时再创建动态代理类 & 实例
  2. 静态代理则是在代理类实现时就指定与目标对象类(RealSubject)相同的接口

在这里插入图片描述
推荐阅读:Carson带你学设计模式:动态代理模式(Proxy Pattern)


到这里基本面试常问的设计模式就全部结束了,主要问的就是单例,手写单例一定要会,里面有加锁机制的原理一定要懂,还有手写观察者模式要能手写

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liknana

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值