Mr. Cappuccino的第18杯咖啡——金三银四面试题之设计模式篇

本文深入探讨了软件设计中的重要概念——设计模式,包括单例模式的多种实现、工厂模式的应用场景、代理模式的原理与分类,以及如何防止单例模式被反射和序列化破解。此外,还介绍了如工厂方法、策略模式、模板方法、外观模式和状态模式等其他常见设计模式,展示了它们在实际开发中的价值和作用。
摘要由CSDN通过智能技术生成

金三银四面试题之设计模式篇

1. 为什么需要使用设计模式?

使用设计模式重构整体代码,可以提高代码的复用性、扩展性,减少代码冗余。

2. 谈谈设计模式的六大原则?
  1. 开闭原则(Open Close Principle):开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
  2. 里氏代换原则(Liskov Substitution Principle):里氏代换原则是面向对象设计的基本原则之一。里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
  3. 依赖倒转原则(Dependence Inversion Principle):这个是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体实现。
  4. 接口隔离原则(Interface Segregation Principle):这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。
  5. 迪米特法则(最少知道原则)(Demeter Principle):为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
  6. 合成复用原则(Composite Reuse Principle):原则是尽量使用合成/聚合的方式,而不是使用继承。
3. 设计模式有哪些?

创建型模式:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式;
结构型模式:适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式;
行为型模式:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

4. 什么是单例模式?

在Java应用程序中,一个类只有一个实例存在。

5. 哪些地方用到过单例模式?
  1. 项目中定义的配置文件;
  2. Servlet对象默认就是单例;
  3. 线程池、数据库连接池;
  4. Spring中Bean对象默认就是单例;
  5. 实现网站计数器;
  6. Jvm内置缓存框架(定义单例HashMap);
  7. 定义枚举常量信息;
6. 单例模式有什么优缺点?

优点:能够节约当前堆内存,不需要频繁new对象,能够快速访问。
缺点:当多个线程访问同一个单例对象的时候可能会存在线程安全问题。

7. 单例模式的写法有哪些?
  1. 懒汉式,线程不安全:当真正需要获取对象的时候,才去创建该单例对象,该写法存在线程问题
package com.singleton;  
  
import java.io.Serializable;  
  
/** 
 * @ClassName Slovenly 
 * @Description 1.懒汉式:线程不安全 
 * @Author honey-袁小康 
 * @Date 2020/6/10 22:06 
 * @Version 1.0 
 */  
public class Slovenly implements Serializable {  
  
    private static Slovenly slovenly = null;  
  
    /** 
     * 私有化构造函数 
     */  
    private Slovenly() {  
  
    }  
  
    /** 
     * 懒汉式:当真正需要该对象的时候才会创建该对象 
     */  
    public static Slovenly getInstance() {  
        if (slovenly == null) {  
            slovenly = new Slovenly();  
        }  
        return slovenly;  
    }  
}  
  1. 懒汉式,线程安全:该写法能够保证线程安全问题,但是获取该单例对象的时候效率非常低
package com.singleton;  
  
/** 
 * @ClassName SlovenlyLock 
 * @Description 2.加锁懒汉式:线程安全,效率低 
 * @Author honey-袁小康 
 * @Date 2020/6/10 22:15 
 * @Version 1.0 
 */  
public class SlovenlyLock {  
  
    private static SlovenlyLock slovenlyLock;  
  
    private SlovenlyLock() {  
  
    }  
  
    public static synchronized SlovenlyLock getInstance() {  
        if (slovenlyLock == null) {  
            slovenlyLock = new SlovenlyLock();  
        }  
        return slovenlyLock;  
    }  
}  
  1. 懒汉式双重检验锁:能够保证线程安全,只会创建该单例对象的时候上锁,获取该单例对象不会上锁,效率比较高。
package com.singleton;  
  
/** 
 * @ClassName DoubleInspectionLock 
 * @Description 3.双重检验锁:线程安全,效率比较高 
 * @Author honey-袁小康 
 * @Date 2020/6/10 22:22 
 * @Version 1.0 
 */  
public class DoubleInspectionLock {  
  
    private static DoubleInspectionLock doubleInspectionLock;  
  
    private DoubleInspectionLock() {  
  
    }  
  
    public static DoubleInspectionLock getInstance() {  
        if (doubleInspectionLock == null) {  
            synchronized (DoubleInspectionLock.class) {  
                if (doubleInspectionLock == null) {  
                    doubleInspectionLock = new DoubleInspectionLock();  
                }  
            }  
        }  
        return doubleInspectionLock;  
    }  
}  
  1. 饿汉式:当class文件被加载就创建该对象,先天性线程安全,浪费内存空间
package com.singleton;  
  
/** 
 * @ClassName Hungry 
 * @Description 饿汉式:先天性线程安全,浪费内存空间 
 * @Author honey-袁小康 
 * @Date 2020/6/10 22:34 
 * @Version 1.0 
 */  
public class Hungry {  
  
    /** 
     * 当class文件被加载的时候就创建该对象 
     */  
    private Hungry() {  
  
    }  
  
    private static Hungry hungry = new Hungry();  
  
    public static Hungry getInstance() {  
        return hungry;  
    }  
}  
  1. 饿汉式(常量)
package com.singleton;  
  
/** 
 * @ClassName HungryConstant 
 * @Description 
 * @Author honey-袁小康 
 * @Date 2020/6/10 22:39 
 * @Version 1.0 
 */  
public class HungryConstant {  
  
    private HungryConstant() {  
  
    }  
  
    public static final HungryConstant HUNGRYCONSTANT = new HungryConstant();  
  
}  
  1. 静态代码块
package com.singleton;  
  
/** 
 * @ClassName StaticCodeBlock 
 * @Description 静态代码块 
 * @Author honey-袁小康 
 * @Date 2020/6/10 22:45 
 * @Version 1.0 
 */  
public class StaticCodeBlock {  
  
    private StaticCodeBlock() {  
  
    }  
  
    private static StaticCodeBlock staticCodeBlock;  
  
    static {  
        staticCodeBlock = new StaticCodeBlock();  
    }  
  
    public static StaticCodeBlock getInstance() {  
        return staticCodeBlock;  
    }  
}  
  1. 静态内部类:结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的
package com.singleton;  
  
/** 
 * @ClassName StaticInnerClass 
 * @Description 静态内部类 
 * @Author honey-袁小康 
 * @Date 2020/6/10 23:50 
 * @Version 1.0 
 */  
public class StaticInnerClass {  
  
    private StaticInnerClass() {  
  
    }  
  
    private static class SingletonHolder {  
        private static final StaticInnerClass STATIC_INNER_CLASS = new StaticInnerClass();  
    }  
  
    public static StaticInnerClass getInstance() {  
        return SingletonHolder.STATIC_INNER_CLASS;  
    }  
}  
  1. 枚举:枚举最安全,不能被反射和序列化破解
package com.singleton;  
 
/** 
 * @ClassName Enum 
 * @Description 
 * @Author honey-袁小康 
 * @Date 2020/6/10 23:45 
 * @Version 1.0 
 */  
public enum Enum {  
  
    /** 
     * 枚举单例 
     */  
    INSTANCE;  
  
    public void getInstance() {  
        System.out.println("getInstance");  
    }  
}  
8. 创建对象有多少种方式?
  1. 直接new对象;
  2. 采用克隆对象;
  3. 使用反射创建对象;
  4. 序列化与反序列化;
9. 有哪些方式可以破解单例?

使用反射和序列化可以破解单例。

10. 如何使用反射破解单例?
try {  
            Class<?> aClass = Class.forName("com.crack.PreventCracking");  
            Constructor<?> constructor = aClass.getDeclaredConstructor();  
            constructor.setAccessible(true);  
            PreventCracking preventCracking2 = (PreventCracking) constructor.newInstance();  
            PreventCracking preventCracking1 = PreventCracking.getInstance();  
            System.out.println(preventCracking1 == preventCracking2);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
11. 如何防止反射破解单例?
package com.crack;  
  
/** 
 * @ClassName PreventCracking 
 * @Description 防止反射破解单例 
 * @Author honey-袁小康 
 * @Date 2020/6/10 23:08 
 * @Version 1.0 
 */  
public class PreventCracking {  
  
    private static PreventCracking preventCracking = null;  
  
    private PreventCracking() throws Exception {  
        if (preventCracking != null) {  
            throw new Exception("该对象已经被创建,请勿重复创建!");  
        } else {  
            preventCracking = this;  
        }  
    }  
  
    public static PreventCracking getInstance() throws Exception {  
        if (preventCracking == null) {  
            preventCracking = new PreventCracking();  
        }  
        return preventCracking;  
    }  
}  
12. 如何使用序列化破解单例?
// 使用序列化破解单例  
        FileOutputStream fos = null;  
        ObjectOutputStream oos = null;  
        ObjectInputStream ois = null;  
        try {  
            // 1.将对象序列化存入到本地文件中  
            fos = new FileOutputStream("d:/code/slovenly.txt");  
            oos = new ObjectOutputStream(fos);  
            Slovenly slovenly1 = Slovenly.getInstance();  
            oos.writeObject(slovenly1);  
            //2.从硬盘中反序列化对象到内存中  
            ois = new ObjectInputStream(new FileInputStream("d:/code/slovenly.txt"));  
            Slovenly slovenly2 = (Slovenly) ois.readObject();  
            System.out.println(slovenly1 == slovenly2);  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            try {  
                if (ois != null) {  
                    ois.close();  
                }  
                if (oos != null) {  
                    oos.close();  
                }  
                if (fos != null) {  
                    fos.close();  
                }  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
13. 如何防止序列化破解单例?
package com.singleton;  
  
import java.io.Serializable;  
  
/** 
 * @ClassName Slovenly 
 * @Description 1.懒汉式:线程不安全 
 * @Author honey-袁小康 
 * @Date 2020/6/10 22:06 
 * @Version 1.0 
 */  
public class Slovenly implements Serializable {  
  
    private static Slovenly slovenly = null;  
  
    /** 
     * 私有化构造函数 
     */  
    private Slovenly() {  
  
    }  
  
    /** 
     * 懒汉式:当真正需要该对象的时候才会创建该对象 
     */  
    public static Slovenly getInstance() {  
        if (slovenly == null) {  
            slovenly = new Slovenly();  
        }  
        return slovenly;  
    }  
  
    /** 
     * 序列化生产回调方法 通过该方法实现反序列化生产单例对象 
     * 
     * @return Object 
     */  
    public Object readResolve() {  
        return slovenly;  
    }  
}  
14. 什么是工厂模式?

工厂模式提供了一种创建对象的最佳方式。在工厂模式中,在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。实现了创建者和调用者分离,工厂模式分为简单工厂、工厂方法、抽象工厂、静态工厂。

15. 哪些地方用到过工厂模式?

Spring IOC创建bean时使用了BeanFactory。

16. 工厂模式有什么优缺点?

优点:代码结构简单;获取产品的过程更加简单;满足了开闭原则,即对拓展开放,对修改关闭;降低程序的耦合性;减少重复冗余代码;减少了使用者因为创建逻辑导致的错误。
缺点:拓展较繁琐,拓展时,需同时改动抽象工厂和工厂实现类。

17. 什么是代理模式?

代理模式主要对方法执行之前与之后实现增强。

18. 哪些地方用到过代理模式?
  1. 日志的采集
  2. 权限控制
  3. 实现aop
  4. Mybatis mapper
  5. Spring的事务
  6. 全局捕获异常
  7. Rpc远程调用接口
  8. 代理数据源
19. 谈谈代理模式的实现原理?

代理模式主要包含三个角色,即抽象主题角色(Subject)、委托类角色(被代理类角色Proxied)以及代理类角色(Proxy),如下图所示:
在这里插入图片描述
抽象主题角色:可以是接口,也可以是抽象类;
委托类角色:真实主题角色,业务逻辑的具体执行者;
代理类角色:内部含有对真实对象RealSubject的引用,负责对真实主题角色的调用,并在真实主题角色处理前后做预处理和后处理。

20. 代理模式分为哪几种?

代理模式分为静态代理模式和动态代理模式;动态代理模式又分为JDK动态代理和Cglib动态代理。

21. 什么是静态代理模式?

由程序员创建或工具生成代理类的源码,再编译代理类。所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。

22. 如何基于接口实现静态代理模式?
23. 如何基于继承实现静态代理模式?
24. 动态代理和静态代理有什么区别?

动态代理不需要写代理类对象,通过程序自动生成,而静态代理需要我们自己写代理类对象。

25. 什么是动态代理模式?

动态代理在实现阶段不用关心代理类,而在运行阶段才指定哪一个对象。
动态代理类的源码是在程序运行期间由JVM根据反射等机制动态生成。

26. 如何实现JDK动态代理?
  1. 创建被代理的接口和类;
  2. 实现InvocationHandler接口,对目标接口中声明的所有方法进行统一处理;
  3. 调用Proxy的静态方法,创建代理类并生成相应的代理对象;
27. 谈谈JDK动态代理的实现原理?

利用拦截器机制,必须实现InvocationHandler接口中的invoke方法实现对目标方法增强。

28. 谈谈手写JDK动态代理的思路?
  1. 创建InvocationHandler接口,定义一个invoke方法;
  2. 使用Java反射技术获取接口下所有的方法,拼接$Proxy0.java代码;
  3. 再将$Proxy0.java编译成class文件,读取到内存中;
29. 谈谈CGLIB动态代理的实现原理?

使用ASM字节码技术,生成子类对目标方法实现增强。
Cglib依赖于ASM字节码技术,直接生成class文件,再使用类加载器读取到程序中;
使用fastclass对被代理类的方法建立索引文件,不需要依赖于反射查找到目标方法,所以效率比Jdk动态代理高。

30. JDK动态代理和CGLIB动态代理有什么区别?
  1. Jdk动态代理利用反射技术生成匿名的代理类走InvokeHandler回调方法实现增强,同时也是一种基于接口的方式实现代理;
  2. Cglib动态代理利用asm字节码技术生成一个子类覆盖其中的方法实现增强,同时采用fastClass机制对整个代理类建立索引,比反射效率要高;
  3. 在Spring中如果需要被代理的对象如果实现了接口采用Jdk动态代理,没有实现接口则使用Cglib动态代理。
31. @Async注解为什么会失效?

使用@Async注解时,应当单独使用一个类进行调用。

  1. 如果在控制类实现的接口上面使用了@Async注解,代码底层会走JDK动态代理,会导致控制类无法注入到SpringMVC容器中,页面会报404,找不到该接口。
  2. 如果在同一个类中,调用加上了@Async注解的方法,会导致@Async没有异步的效果,虽然代码底层走的是CGLIB动态代理,能够注册到SpringMVC容器中,但是两个方法在同一个类中,会导致@Async注解没走代理类,无法对方法实现增强。
  3. 如果没有使用@EnableAsync注解的话,@Async注解也会失效。
32. @Transaction注解为什么会失效?

使用@Transaction注解时,应当单独使用一个类进行调用。

  1. 如果在同一个类中,调用加上了@Transaction注解的方法,会导致注解失效。
  2. 如果在方法中使用try-catch语句,Aop捕获不到异常,代码不会回滚,需要手动回滚。
33. 如何基于JDK动态代理手写@Async和@Transaction?
34. 如何基于AOP手写@Async和@Transaction?
35. 什么是装饰模式?

不改变原有代码的基础之上,新增附加功能。
在这里插入图片描述
(1)抽象组件:定义一个抽象接口,来规范准备附加功能的类
(2)具体组件:将要被附加功能的类,实现抽象构件角色接口
(3)抽象装饰者:持有对具体构件角色的引用并定义与抽象构件角色一致的接口
(4)具体装饰:实现抽象装饰者角色,负责对具体构件添加额外功能。

36. 哪些地方使用过装饰模式?

多级缓存设计;mybatis中的一级与二级缓存;IO流。

37. 如何基于装饰模式实现多级缓存?
38. 什么是观察者模式?

一个对象状态改变,通知给其他所有的对象。
在这里插入图片描述

39. 哪些地方使用过观察者模式?

Zk的事件监听;分布式配置中心刷新配置文件;业务中群发不同渠道的消息。

40. 如何基于观察者模式实现异步多渠道群发框架?
41. 什么是责任链模式?

定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。其过程实际上是一个递归调用。

  1. 抽象处理者(Handler)角色:定义出一个处理请求的接口。如果需要,接口可以定义出一个方法以设定和返回对下家的引用。这个角色通常由一个Java抽象类或者Java接口实现。
  2. 具体处理者(ConcreteHandler)角色:具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家。
42. 责任链模式有什么优缺点?

优点:
责任链模式的最主要功能就是:动态组合,请求者和接受者解耦。
请求者和接受者松散耦合:请求者不需要知道接受者,也不需要知道如何处理。每个职责对象只负责自己的职责范围,其他的交给后继者。各个组件间完全解耦。
动态组合职责:责任链模式会把功能分散到单独的职责对象中,然后在使用时动态的组合形成链,从而可以灵活的分配职责对象,也可以灵活的添加改变对象职责。

缺点:
产生很多细粒度的对象:因为功能处理都分散到了单独的职责对象中,每个对象功能单一,要把整个流程处理完,需要很多的职责对象,会产生大量的细粒度职责对象。
不一定能处理:每个职责对象都只负责自己的部分,这样就可能出现某个请求,即使把整个链走完,都没有职责对象处理它。这就需要提供默认处理,并且注意构造链的有效性。

43. 哪些地方用到过责任链模式?
  1. 多条件流程判断,权限控制;
  2. ERP系统流程审批:总经理、人事经理、项目经理;
  3. 风控系统:失信名单→信用卡是否逾期→蚂蚁信用积分650;
  4. 在Java过滤器Filter中客户端发送请求到服务器端,会经过参数过滤、session过滤、表单过滤、隐藏过滤、检测请求头过滤;
    网关权限控制:Api接口限流→黑名单拦截→用户会话验证→参数过滤。
44. 如何基于责任链模式实现企业级网关系统?
45. 什么是策略模式?

策略模式是对算法的包装,是把使用算法的责任和算法本身分割开来,委派给不同的对象管理,最终可以实现解决多重if判断问题。

  1. 环境(Context)角色:持有一个Strategy的引用。
  2. 抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
  3. 具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。

定义策略接口->实现不同的策略类->利用多态或其他方式调用策略

46. 哪些地方用到过策略模式?
  1. 异步实现发送短信 比如阿里云、腾讯云、其他短信渠道等;
  2. 聚合支付系统 银联支付、支付宝、微信支付等;
  3. 联合登陆 QQ、钉钉、微信联合登陆渠道等;
47. 如何基于策略模式实现多渠道消息平台?
48. 什么是模板方法模式?
  1. 定义一个具有共同行为的骨架,而将部分步骤的实现在子类中完成。
  2. 模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术,没有关联关系。因此,在模板方法模式中,只有继承关系。

核心设计要点:
AbstractClass : 抽象类,定义并实现一个模板方法。这个模板方法定义了算法的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类去实现。
ConcreteClass : 实现类,实现父类所定义的一个或多个抽象方法。

49. 什么是外观模式?

外观模式(Facade):他隐藏了系统的复杂性,并向客户端提供了一个可以访问系统的接口。这种类型的设计模式属于结构型模式。为子系统中的一组接口提供了一个统一的访问接口,这个接口使得子系统更容易被访问或者使用。

在这里插入图片描述

简单来说,该模式就是把一些复杂的流程封装成一个接口供给外部用户更简单的使用。这个模式中,设计到3个角色。
1)门面角色:外观模式的核心。它被客户角色调用,它熟悉子系统的功能。内部根据客户角色的需求预定了几种功能的组合。
2)子系统角色:实现了子系统的功能。它对客户角色和Facade时未知的。它内部可以有系统内的相互交互,也可以由供外界调用的接口。
3)客户角色:通过调用Facede来完成要实现的功能。

50. 什么是状态模式?

状态模式允许一个对象在其内部状态改变的时候改变其行为。这个对象看上去就像是改变了它的类一样。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值