java面试——设计模式

面向对象的特点是 可维护、可复用、可扩展、灵活性好,它最强大的地方在于:随着业务变得越来越复杂,面向对象依然能够使得程序结构良好,而面向过程却会导致程序越来越臃肿。

让面向对象保持结构良好的秘诀就是设计模式,面向对象结合设计模式,才能真正体会到程序变得可维护、可复用、可扩展、灵活性好。设计模式对于程序员而言并不陌生,每个程序员在编程时都会或多或少的接触到设计模式。无论是在大型程序的架构中,亦或是在源码的学习中,设计模式都扮演着非常重要的角色。

1. 说一说设计模式的六大原则
  • 开闭原则:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
    在面向对象的编程中,开闭原则是最基础的原则,起到总的指导作用,其他原则(单一职责、里氏替换、依赖倒置、接口隔离、迪米特法则)都是开闭原则的具体形态,即其他原则都是开闭原则的手段和工具。开闭原则的重要性可以通过以下几个方面来体现。
    开闭原则提高复用性、可维护性、灵活性,并且易于测试。
    例:搜狗输入法下载新皮肤,可以定义一个皮肤的抽象类,每个新皮肤都是这个抽象类的子类,这样添加新主题就不需要修改源代码,所以它满足开闭原则。
  • 里氏替换原则:子类应该可以完全替换父类。也就是说在使用继承时,只扩展新功能,而不要破坏父类原有的功能。
    例:正方形不是长方形,违反里氏替换原则可能导致使用多态错误,可以抽象出来个四边形接口。
  • 依赖倒置原则:细节应该依赖于抽象,抽象不应依赖于细节,高层模块和底层模块都应该依赖其抽象。把抽象层放在程序设计的高层,并保持稳定,程序的细节变化由低层的实现层来完成。
    例:组装电脑,应该让电脑的组件依赖各个组件的抽象,而不是直接依赖于实现类。
  • 迪米特法则:又名“最少知道原则”,一个类不应知道自己操作的类的细节,换言之,只和朋友谈话,不和朋友的朋友谈话。
    例:明星日常事务由经纪人负责,其中经济人是明星的朋友,而日常事务遇见的是陌生的人,适合用迪米特法则
  • 接口隔离原则:客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。
    例:一个类有三种方法,现在要新建一个类只实现其中两种方法,应该将这三种方法抽象成三个接口。
  • 单一职责原则:一个类只做一件事,一个类应该只有一个引起它修改的原因。
    该原则提出了对对象职责的一种理想期望,对象不应该承担太多职责,正如人不应该一心分为二用。唯有专注,才能保证对象的高内聚;唯有单一,才能保证对象的细粒度。对象的高内聚与细粒度有利于对象的重用。一个庞大的对象承担了太多的职责,当客户端需要该对象的某一个职责时,就不得不将所有的职责都包含进来,从而造成冗余代码。
2. 什么是单例模式?手写一个单例模式

单例模式非常常见,某个对象全局只需要一个实例时,就可以使用单例模式。它的优点也显而易见:

  • 它能够避免对象重复创建,节约空间并提升效率
  • 避免由于操作不同实例导致的逻辑错误

单例模式有两种实现方式:饿汉式和懒汉式。

饿汉式单例模式:

public class Singleton {
    private static Singleton instance = new Singleton();
    // 私有构造方法,保证外界无法直接实例化。
    private Singleton() {}
    // 通过公有的静态方法获取对象实例
    public static Singleton getInstace() {
        return instance;
    }
}

线程安全懒汉式单例模式(双重检查锁):

public class Singleton { 
    //私有构造方法 
    private Singleton() {} 
    private static volatile Singleton instance; 
    //对外提供静态方法获取该对象 
    public static Singleton getInstance() { 
        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际 
        if(instance == null) { 
            synchronized (Singleton.class) { 
                //抢到锁之后再次判断是否为空 
                if(instance == null) { 
                    instance = new Singleton(); 
                } 
            } 
        }
        return instance; 
    } 
}
3.说一说你对工厂模式的理解

工厂模式一共有三种:

  • 简单工厂模式
    直接 new 对象的方式相当于当我们需要一个苹果时,我们需要知道苹果的构造方法,需要一个梨子时,需要知道梨子的构造方法。更好的实现方式是有个水果工厂,我们告诉工厂要什么种类的水果,水果工厂将我们需、要的水果制造出来给我们就可以了。这样我们就无需知道苹果、梨子是怎么生产,只用和水果工厂打交道即可 。
    简单工厂模式其实并不算是一种设计模式,更多的时候是一种编程习惯。简单工厂的实现思路是,定义一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。简单工厂的适用场景是:
  1. 需要创建的对象较少
  2. 客户端不关心对象的创建过程。

缺点:

  1. 生产产品过多会使工厂过于庞大
  2. 生产新产品必须要添加新的分支,违背开闭原则
  • 工厂方法模式
    为了解决简单工厂模式的这两个弊端,工厂方法模式应运而生,它规定每个产品都有一个专属工厂。比如苹果有专属的苹果工厂,梨子有专属的梨子工厂。
    可以解决简单工厂模式两个弊端。
  • 抽象工厂模式
    可以将两个工厂提出一个抽象类IFactory,可以看到,我们在创建时指定了具体的工厂类后,在使用时就无需再关心是哪个工厂类,只需要将此工厂当作抽象的 IFactory 接口使用即可。这种经过抽象的工厂方法模式被称作抽象工厂模式。
    由于客户端只和 IFactory 打交道了,调用的是接口中的方法,使用时根本不需要知道是在哪个具体工厂中实现的这些方法,这就使得替换工厂变得非常容易。
    例:实际上抽象工厂模式主要用于替换一系列方法。例如将程序中的 SQL Server 数据库整个替换为 Access 数据库,使用抽象方法模式的话,只需在 IFactory 接口中定义好增删改查四个方法,让 SQLFactory 和 AccessFactory 实现此接口,调用时直接使用 IFactory 中的抽象方法即可,调用者无需知道使用的什么数据库,我们就可以非常方便的整个替换程序的数据库,并且让客户端毫不知情。
4. 简单工厂模式和抽象工厂模式有什么区别?

简单工厂模式其实并不算是一种设计模式,更多的时候是一种编程习惯。简单工厂的实现思路是,定义一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。

工厂方法模式是简单工厂的仅一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。也就是说每个对象都有一个与之对应的工厂。工厂方法的实现思路是,定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。

抽象工厂模式是工厂方法的仅一步深化,在这个模式中的工厂类不单单可以创建一个对象,而是可以创建一组对象。这是和工厂方法最大的不同点。抽象工厂的实现思路是,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

5. 什么是策略模式?

策略模式(Strategy Pattern):定义了一系列算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。

策略模式用一个成语就可以概括 —— 殊途同归。当我们做同一件事有多种方法时,就可以将每种方法封装起来,在不同的场景选择不同的策略,调用不同的方法。

例:排序算法有许多种,如冒泡排序、选择排序、插入排序,算法不同但目的相同,我们可以将其定义为不同的策略,让用户自由选择采用哪种策略完成排序。

策略模式还有一个弊端:每 new 一个对象,相当于调用者多知道了一个类,增加了类与类之间的联系,不利于程序的松耦合。所以使用策略模式时,更好的做法是与工厂模式结合,将不同的策略对象封装到工厂类中,用户只需要传递不同的策略类型,然后从工厂中拿到对应的策略对象即可。接下来我们就来一起实现这种工厂模式与策略模式结合的混合模式。

enum SortStrategy {
    BUBBLE_SORT,
    SELECTION_SORT,
    INSERT_SORT
}

// 使用简单工厂模式
class Sort implements ISort {

    private ISort sort;

    Sort(SortStrategy strategy) {
        setStrategy(strategy);
    }

    @Override
    public void sort(int[] arr) {
        sort.sort(arr);
    }

    // 客户端通过此方法设置不同的策略
    public void setStrategy(SortStrategy strategy) {
        switch (strategy) {
            case BUBBLE_SORT:
                sort = new BubbleSort();
                break;
            case SELECTION_SORT:
                sort = new SelectionSort();
                break;
            case INSERT_SORT:
                sort = new InsertSort();
                break;
            default:
                throw new IllegalArgumentException("There's no such strategy yet.");
        }
    }
}
6. 说一说你对观察者模式的理解

观察者模式非常常见,近年来逐渐流行的响应式编程就是观察者模式的应用之一。观察者模式的思想就是一个对象发生一个事件后,逐一通知监听着这个对象的监听者,监听者可以对这个事件马上做出响应。

观察者模式具有以下几个优点:

  • 观察者和被观察者之间是抽象耦合。被观察者角色所知道的只是一个具体观察者集合,每一个具体观察者都符合一个抽象观察者的接口。被观察者并不认识任何一个具体的观察者,它只知道它们都有一个共同的接口。由于被观察者和观察者没有紧密的耦合在一起,因此它们可以属于不同的抽象化层次,且都非常容易扩展。
  • 支持广播通信。被观察者会向所有登记过的观察者发出通知,这就是一个触发机制,形成一个触发链。

观察模式的缺点如下:

  • 如果一个主题有多个直接或间接的观察者,则通知所有的观察者会花费很多时间,且开发和调试都比较复杂。
  • 如果在主题之间有循环依赖,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式时要特别注意这一点。
  • 如果对观察者的通知是通过另外的线程进行异步投递,系统必须保证投递的顺序执行。
  • 虽然观察者模式可以随时使观察者知道所观察的对象发生了变化,但是观察者模式没有提供相应的机制使观察者知道所观察的对象是如何发生变化。

观察者模式的应用场景如下:

  • 关联行为场景。
  • 事件多级触发场景。
  • 跨系统的消息交换场景,如消息队列的处理机制。

例:警察抓小偷,警察是观察者,小偷是被观察者

7. 说一说你对责任链模式的了解

责任链模式(Chain of Responsibility Pattern)是一种常见的行为模式,它的目的是使多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。

责任链主要用于处理 职责相同,程度不同的类

责任链模式的优点如下:

  • 责任链模式将请求和处理分开,请求者不知道是谁处理的,处理者可以不用知道请求的全貌。
  • 提高系统的灵活性。

责任链模式的缺点如下:

  • 降低程序的性能,每个请求都是从链头遍历到链尾,当链比较长的时候,性能会大幅下降。
  • 不易于调试,由于采用了类似递归的方式,调试的时候逻辑比较复杂。

责任链模式的应用场景如下:

  • 一个请求需要一系列的处理工作。
  • 业务流的处理,例如,文件审批。
  • 对系统进行补充扩展。

例:三个不同水平的程序员解决不同难度bug

8. 说一说装饰器模式和适配器模式的区别

装饰器的目的是动态地给一个对象添加一些额外的职责,这个对象的类型不会发生变化,但是行为却发生了改变。

适配器的目的是将一个类的接口变换成客户端所期待的另一种接口,就是可以将一个对象包装成另外的一个接口。

9. Spring框架中用到了哪些设计模式?

Spring框架在实现时运用了大量的设计模式,常见的有如下几种:

  1. 简单工厂
    Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。
  2. 工厂方法
    实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean的getOjbect()方法的返回值。
  3. 单例模式
    Spring依赖注入Bean实例默认是单例的。Spring的依赖注入(包括lazy-init方式)都是发生在AbstractBeanFactory的getBean里。getBean的doGetBean方法调用getSingleton进行bean的创建。
  4. 适配器模式
    SpringMVC中的适配器HandlerAdatper,它会根据Handler规则执行不同的Handler。即DispatcherServlet根据HandlerMapping返回的handler,向HandlerAdatper发起请求处理Handler。HandlerAdapter根据规则找到对应的Handler并让其执行,执行完毕后Handler会向HandlerAdapter返回一个ModelAndView,最后由HandlerAdapter向DispatchServelet返回一个ModelAndView。
  5. 装饰器模式
    Spring中用到的装饰器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。
  6. 代理模式
    AOP底层就是动态代理模式的实现。即:切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象创建动态的创建一个代理对象。SpringAOP就是以这种方式织入切面的。
  7. 观察者模式
    Spring的事件驱动模型使用的是观察者模式,Spring中Observer模式常用的地方是listener的实现。
  8. 策略模式
    Spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力,Spring 框架本身大量使用了 Resource 接口来访问底层资源。Resource 接口是具体资源访问策略的抽象,也是所有资源访问类所实现的接口。
  9. 模板方法模式
    Spring模板方法模式的实质,是模板方法模式和回调模式的结合,是Template Method不需要继承的另一种实现方式。Spring几乎所有的外接扩展都采用这种模式。
10. 23种设计模式简要介绍

构建型模式:

  • 工厂方法模式:为每一类对象建立工厂,将对象交由工厂创建,客户端只和工厂打交道。
  • 抽象工厂模式:为每一类工厂提取出抽象接口,使得新增工厂、替换工厂变得非常容易。
  • 建造者模式:用于创建构造过程稳定的对象,不同的 Builder 可以定义不同的配置。
  • 单例模式:全局使用同一个对象,分为饿汉式和懒汉式。懒汉式有双检锁和内部类两种实现方式。
  • 原型模式:为一个类定义 clone 方法,使得创建相同的对象更方便。

结构型模式:

  • 适配器模式:用于有相关性但不兼容的接口
  • 桥接模式:用于同等级的接口互相组合
  • 组合模式:用于整体与部分的结构
  • 外观模式:体现封装的思想
  • 享元模式:体现面向对象的可复用性
  • 代理模式:主要用于对某个对象加以控制
  • 责任链模式:处理职责相同,程度不同的对象,使其在一条链上传递

行为型模式:

  • 命令模式:封装“方法调用”,将行为请求者和行为实现者解耦
  • 解释器模式:定义自己的语法规则
  • 迭代器模式:定义 next() 方法和 hasNext() 方法,让外部类使用这两个方法来遍历列表,以达到隐藏列表内部细节的目的
  • 中介者模式:通过引入中介者,将网状耦合结构变成星型结构
  • 备忘录模式:存储对象的状态,以便恢复
  • 观察者模式:处理一对多的依赖关系,被观察的对象改变时,多个观察者都能收到通知
  • 状态模式:关于多态的设计模式,每个状态类处理对象的一种状态
  • 策略模式:殊途同归,用多种方法做同一件事
  • 模板方法模式:关于继承的设计模式,父类是子类的模板
  • 访问者模式:将数据的结构和对数据的操作分离