Java设计模式高频面试题汇总:单例、工厂与模板模式详解
简介:

本期文章会汇总与 java 设计模式相关的面试题,这些面试题出现的频率较高。我们要给最近有找工作需求的朋友们总结一下,以此来帮助大家全面地掌握设计模式的核心知识点,进而提升自身的竞争力,为大家的面试之旅提供保障。
常见的设计模式说一下(高频)
一个类只能有一个实例,模式分为饿汉模式和懒汉模式以及枚举。饿汉模式是迫切加载,懒汉模式是延迟加载。
工厂模式能够隐藏产品复杂的创建过程,还能实现生产功能的复用,从而使产品生产更为高效。它分为简单工厂,这种工厂需要来回切换生产线;还有工厂方法,即开设新的生产线;以及抽象工厂,它制定创建产品的接口,让子工厂去选择创建哪种产品。
Spring 中的各种 BeanFactory 在创建 bean 时都用到了。
定义一个算法的骨架或者流程,这就是模板模式。不同的实例在实现方式上有所不同,它会将某个或多个具体的实现延迟到子类中。例如,RedisTemplate 实现了 RedisOperations,ElasticSearchTemplate 实现了 ElasticsearchOperations。
代理模式:不会直接使用实际对象。而是通过调用代理对象,来间接调用实际对象。它主要被用作对实际对象的增强。代理模式分为静态代理、JDK 动态代理和 CGLIB 动态代理。例如,Spring 的 AOP 原理就是动态代理。当目标对象实现了接口时,会使用 JDK 动态代理;当目标对象没有实现接口时,会使用 CGLIB 动态代理。
适配器模式是一种中间类,它能将不兼容的接口转换为可兼容的接口。像 HandlerInterceptorAdapter 这样的,当我们定义拦截器时,无需覆写 HandlerInterceptor 中的所有方法,因为适配器类已经为我们做了空实现。JDK1.8 之后,接口中被增加了默认方法,且这些默认方法可以有方法体。所以,那些原本用于实现接口的适配器类已经没有作用了。
当一个对象的状态发生改变的时候,所有依赖于它的对象都会被通知到,并且会被自动更新。例如在 Spring 中就有 ApplicationListener 这种情况。
什么是单例,如何实现
一个类仅能有一个实例。这种情况主要用于以下两种场景:一是需要频繁使用的对象,通过这种方式可避免频繁初始化和销毁,以提高性能;二是在资源需要相互通信的环境中。
主要实现方式有,饿汉模式,懒汉模式,枚举,静态内部类
饿汉模式中,构造器被私有化,同时提供了静态获取实例的方法。在类加载过程中,就会将这个单例对象进行实例化。需要把构造方法设置为私有化,定义一个成员变量,然后 new 一个该类的实例作为初始值,最后提供一个公共的静态方法来获取这个实例。
//单利模式 饿汉 :工具类
class JDBCUtil{
定义成员变量,将 new 出来的实例作为初始值。对于饿汉式,它是一来就创建实例并赋予初始值。
创建了一个名为 instance 的 JDBCUtil 类的实例。
//1.私有构造器
private JDBCUtil(){}
//3.提供获取实例方法
通过调用 public static 方法来获取 JDBCUtil 的实例。
return instance;
}
}
System 输出 JDBCUtil 的获取实例的结果。
懒汉模式,是在使用的时候才去创建这个单例对象。需要把构造方法设置为私有化。要定义一个该类的成员变量,并且不给它赋初始值。还要提供一个能够获取实例的公共静态方法。特别要留意的是,这个方法必须要保证在多线程环境下的并发安全性,能够通过 DCL 加上 volatile 关键字来予以解决。
//单利模式 懒汉 :工具类
class JDBCUtil{
//1.定义成员变量,不赋初始值
静态私有且易失的 JDBCUtil 实例初始值为 null 。
//1.私有构造器
private JDBCUtil(){}
//2.提供获取实例方法
获取 JDBCUtil 的实例的方式是通过静态方法。具体来说,就是调用 getInstance 这个静态方法来获取 JDBCUtil 的实例。
//第一次调用时创建对象
if(instance == null){
同步代码块可确保创建实例的代码具有原子性,并且只会创建一个实例。
在 JDBCUtil.class 上进行同步操作。
if(instance == null){
instance = new JDBCUtil();
}
}
}
return instance;
}
}
System.out.println(JDBCUtil.getInstance());
枚举,直接在枚举中定义字段,它就是单例并且线程安全的
//单利:枚举
enum JDBCUtil{
INSTANCE;
}
System 输出 JDBCUtil 的 INSTANCE。
静态内部类的方式是,在一个类里面创建一个静态内部类,接着在这个静态内部类中创建一个目标类的静态成员变量,并为其 new 一个实例作为初始值。随后在目标类中定义一个获取实例的静态方法,该方法返回的就是静态内部类中的成员变量。这种方式具备能保证线程安全以及可以实现延迟加载的优点,然而其缺点在于传参不是很方便。
模板模式的作用
定义一个算法骨架,然后把某个或多个具体的实现延迟到子类里面去,这样就能让子类在不改动当前算法结构的情况下,重新去定义当前算法的某些特定步骤。
比如考试中所有考生的试卷都一样,答案由每个考生自己完成
什么是适配器模式
将不兼容的接口转换为可兼容的接口的中间类
HandlerInterceptorAdapter ,在定义拦截器时,我们无需覆写 HandlerInterceptor 中的所有方法,原因是适配器类为我们做了空实现。然而,自 JDK1.8 之后,接口中增加了默认方法,且可以有方法体,所以这些适配器类便不再起作用了。
什么是代理模式?有几种代理?
不直接运用实际对象,而是借助调用代理对象来间接调用实际对象,其主要作用是对实际对象进行增强,可分为静态代理、JDK 动态代理以及 CGLIB 动态代理。
JDK动态代理和CGLIB动态代理的区别?(高频)
JDK 动态代理是由 jdk 所提供的,我们能够直接对其进行使用。然而,CGLIB 则需要导入第三方的库。
JDK 动态代理通过反射机制生成一个匿名类,这个匿名类实现了代理接口。在调用目标方法之前,会调用 InvokeHandler 来进行处理。
CGLIB 动态代理首先会加载目标类的 class 文件,接着对其字节码进行修改,从而生成子类以实现代理功能。
代理模式详解
代理模式的意思是,客户端不会直接去调用实际的对象。而是通过调用代理对象,从而间接地调用实际的对象。这样一来,我们就能够在代理调用被代理对象的之前、之后,或者在出现错误时加入我们自己的逻辑。
举例:经纪人和明星 ,菜鸟驿站代收快递。
代码举例
1. 抽象的基类
//抽象基类,定义流程 - 喝水的流程
抽象类 AbstractDrinkWaterTemplate 具有抽象性。它为饮水相关的操作提供了一个模板框架。通过这个类,可以定义饮水过程中的一些通用步骤和逻辑。这些步骤和逻辑可以在具体的子类中进行实现和扩展。子类可以根据实际需求,对饮水的具体行为进行定制和修改。
//喝水的流程
void drinkWaterProcess(){
//烧
System.out.println("烧水...");
//倒
System.out.println("倒水...");
//泡
pourWater();
//喝
drinkWater();
}
abstract void pourWater();
abstract void drinkWater();
}
2. 子类实现一个Coffee类
公共类 Coffee 继承自 AbstractDrinkWaterTemplate 类。
@Override
void pourWater() {
System.out.println("泡咖啡...");
}
@Override
void drinkWater() {
System.out.println("喝咖啡...");
}
}
3.子类实现Team类
公共类 Team 继承自抽象类 AbstractDrinkWaterTemplate 。
@Override
void pourWater() {
System.out.println("泡茶...");
}
@Override
void drinkWater() {
System.out.println("喝茶...");
}
}
测试代码
公共类 DrinkWaterTemplateTest 。
在主函数中进行相关操作和逻辑处理。
new Coffee().进行喝水过程();
然后换行。
new Team 完成喝水的流程;
}
}
请提供需要改写的横线下的句子呀,没有具体内容可进行改写呢。
1.静态代理
代理分为动态代理和静态代理。首先来研究简单的静态代理。
实体接口
//主体
public interface Subject {
//访问
void visit();
}
被代理真实对象
公共类 RealSubject 实现了 Subject 接口。
这个私有字符串 name 的值是“我是真实对象”。
@Override
public void visit() {
//真实对象执行...
System.out.println(name);
}
}
代理类
//代理对象
公共类 ProxySubject 实现了 Subject 接口。
//代理对象持有真实对象
private Subject subject;
//构造代理对象需要传入被代理的真实对象
公共的代理主题(主题 subject)。 它接收一个主题作为参数。 这个主题将被代理主题所使用。 用于在代理主题中对真实主题进行代理操作。 例如,在代理主题中可以对真实主题的方法进行拦截、增强或转发等操作。 这样可以在不改变真实主题代码的情况下,为其添加额外的功能或控制逻辑。 例如,在访问真实主题之前或之后进行一些额外的处理,如日志记录、权限验证等。 这有助于提高系统的可扩展性和可维护性。 同时,也可以通过代理主题来隐藏真实主题的复杂性,提供更简单的接口给外部调用者。 外部调用者只需与代理主题交互,而无需直接与真实主题打交道。 这样可以降低系统的耦合度,提高系统的灵活性。
this.subject = subject;
}
@Override
public void visit() {
//1.执行前
//2.调用真实对象的方法
subject.visit();
//3.执行后
}
}
创建代理对象
public class Client {
public static void main(String[] args) {
//创建代理
ProxySubject 的实例 subject 被创建,它的构造函数参数是一个新的 RealSubject 的实例。
subject.visit();
}
}
静态代理的优缺点如下:每个类都需创建一个代理类,在项目开发中,要为众多类创建代理类,这对于项目开发来说无疑是一项很繁琐的工作。而动态代理则可以在代码运行过程中动态生成代理类。
2.JDK动态代理
动态代理与静态代理不同,它是依据代理的对象来动态创建代理类。如此一来,便能避免静态代理中代理类接口过多的状况。在 Java1.3 中提供了动态代理,使我们能够在代码运行期间实现一个接口的代理实例。这个功能刚出现的时候,几乎没有什么特别大的实际用处,然而后来却发现,它简直就是为实现 AOP 而专门设计的。
大家要注意,jdk 的动态代理仅能实现有接口的代理。然而,在我们的开发过程中,很多时候可能会碰到需要代理没有接口的类的情况(创建的代理对象就是该类的子类),就像我们学习的 Hibernate 中的延时加载就是采用这种方式。那么,Spring 是怎样解决这个问题的呢?Spring 通过两种方式来实现动态代理:
所以,现在我们要开始学习如何使用这两种模式去完成动态代理。
JDK 动态代理仅能代理具有接口的类,以下是完成 JDK 代理的主要类:
可以在 jdk 文档中找到 java.lang.reflect.Proxy 这个类。
java.lang.reflect 包中的 InvocationHandler 是代理调用处理程序的接口。
创建JDKProxy类:
引入 Java 语言中的反射机制相关的类 `java.lang.reflect.InvocationHandler` 。
引入 Java 语言的反射机制中的 Method 类。
导入 Java 的反射机制中的代理类。
/**
InvocationHandler 是一个接口,它实现了代理实例的调用处理程序。
*/
类 JdkProxy 实现了 InvocationHandler 接口。
//定义真实主题角色:目标对象
private Object targetObject;
//传入事务管理器
private TxManager txManager;
创建 JdkProxy 实例时,需要传入目标对象 targetObject 和事务管理器 txManager 。
this 的 targetObject 被赋值为 targetObject 。
this.txManager = txManager;
}
/**
* proxy:经过jdk的代理对象(基本上没有作用)
* method:实际执行的方法
* args:方法中的参数
*/
@Override
调用目标对象的指定方法,并传入参数数组,最后返回结果,若发生异常则抛出。具体来说,就是传入目标对象 targetObject、方法 method 以及参数数组 args,然后进行方法的调用操作,若有异常产生则将其抛出。
Object result = null; //返回的结果
try {
txManager.begin();
通过方法调用目标对象并传入参数,从而执行直接对象的方法,即 result = method.invoke(targetObject, args) 。
txManager.commit();
} catch (Exception e) {
txManager.rollback();
e.printStackTrace();
}finally{
txManager.close();
}
return result;
}
/**
* 创建一个代理对象
* @return
*/
public Object createProxy(){
通过以下方式创建代理实例:使用 Proxy.newProxyInstance 方法来创建代理实例。
获取类加载器,只需拿到一个即可,即 this.getClass().getClassLoader()。
targetObject 所属类的 getInterfaces() 方法,这是实现类的接口集合,因为一个类可以实现多个接口。
这个代理实例的调用处理程序是 InvocationHandler 的实现类。
);
}
}
测试功能
@Test
最后对测试结果进行分析和评估,以确定代理的功能是否正常。
User user = new User();
//真实主题角色对象
创建一个 EmployeeService 接口的实例,该实例指向 EmployeeServiceImpl 类的对象,即 EmployeeServiceImpl 类实现了 EmployeeService 接口并创建了其实例,赋值给变量 employeeService 。
//事务管理器
创建了一个 TxManager 类的实例 txManager 。
创建一个处理代理功能的类,这是我们自定义的。在这个类中,我们添加了一个方法,通过这个方法可以直接创建代理对象。
创建了一个 JdkProxy 实例,该实例名为 jdkProxy,它的构造函数传入了 employeeService 和 txManager 两个参数。
//获取代理对象
创建代理的代码如下:IEmployeeService proxy 等于 (IEmployeeService)jdkProxy.createProxy() 。
proxy.save(user);
}
3.CGLIB动态代理
Cglib 的功能与 javassist - 3.18.1 - GA.jar 的功能类似,都是字节码增强。在 Hibernate3.2 之前,原本就是使用 cglib 来进行字节码增强的。以下是完成 CGLIB 的类:
CGLIBProxy类代码
import java.lang.reflect.Method;
从 org.springframework.cglib.proxy 这个包中导入 Enhancer 类。
从 org.springframework.cglib.proxy 包中导入 MethodInterceptor 类。
引入了 org.springframework.cglib.proxy 包中的 MethodProxy 类。
公共类 CglibProxy 实现了 MethodInterceptor 接口。
//定义参数,接收真实的目标对象
private Object targetObject;
//事务对象
private TxManager txManager;
创建 CglibProxy 时需要传入目标对象 targetObject 和事务管理器 txManager 。
this.targetObject = targetObject;
this.txManager = txManager;
}
/**
CGLIB 代理后的对象是 proxyObject,通常不会使用它。
* method:真实对象的方法
* args:方法的参数
方法代理是 CGLIB 代理后的方法,通常是不被使用的。
*/
@Override
公共的 Object 会对传入的 Object proxyObject、Method method、Object[] args 以及 MethodProxy methodProxy 进行拦截操作,并且会抛出 Throwable 异常。
Object result = null; //返回的结果
try {
txManager.begin();
result = method.invoke(targetObject, args); //执行直接对象的方法
txManager.commit();
} catch (Exception e) {
txManager.rollback();
e.printStackTrace();
}finally{
txManager.close();
}
return result;
}
/**
* 创建一个代理对象
* @return
*/
public Object createProxy(){
//创建增强器
创建了一个名为 Enhancer 的对象 Enhancer enhancer 。
//创建的代理就是咱们真实目标对象的子类
enhancer 把目标对象的类设置为它的超类。
MethodInterceptor 是一个 Callback 回调。
enhancer.setCallback(this);
//创建一个代理对象并返回
return enhancer.create();
}
}
测试代码
@Test
公共的 testProxy 方法具备抛出异常的功能。
User user = new User();
//真实主题角色对象
创建了一个名为 employeeService 的 EmployeeServiceImpl 类的实例。
//事务管理器
创建了一个 TxManager 的实例,名为 txManager。
//创建Cglib代理对象
CglibProxy 被创建出来,它是一个新的 CglibProxy 实例,传入了 employeeService 和 txManager 作为参数。
//拿到代理对象
EmployeeServiceImpl 这个 proxy 是通过 (EmployeeServiceImpl)cglibProxy.createProxy() 生成的。
proxy.save(user);
}
工厂模式
工厂模式源自生活中的工厂。工厂的功能是生产某种或多种产品。它隐藏了(封装了)产品复杂的创建过程,还能实现生产功能的复用,使产品的生产变得更加便捷和高效。在 Java 语言中,工厂模式是用于生成对象的。
工厂分为三种:简单工厂,工厂方法,抽象工厂。
该模式用于封装和管理对象的创建,是一种创建型模式。
1.简单工厂
这种模式在对象创建管理方面最为简便,原因在于它只是对不同类对象的创建进行了一层较为浅显的封装。简单工厂模式是由一个工厂对象来决定创建出哪一种产品类的实例。其 UML 类图呈现如下:
简单工厂的优缺点
2.工厂方法
简单工厂模式中工厂负责生产所有产品,而工厂方法模式则是把生成具体产品的任务分派给具体的产品工厂。也就是说,每种类型都有一个专门的工厂,并且为每个工厂再抽象出一个工厂。
一个工厂既要创建小米,又要创建华为,这本身就已经比较混乱了。如果再新增一个产品苹果,在已有的工厂继续增加生产苹果的业务,就会把生产线搞得更乱。所以我打算对产品进行分类,为每一种产品都建立一个工厂,每个工厂只需要创建特定的某一种产品,这样生产线就会变得有条理。其 UML 类图如下:
3.抽象工厂
上面两种模式在工厂进行抽象拆分时,都仅仅是针对某一类产品 Phone(AbstractProduct)。倘若要生成另一种产品 PC,应当如何进行表示呢?
最简单的方式是将 2 中所介绍的工厂方法模式完整地复制一份,这次要生产的是 PC。然而,这也就意味着我们必须完全复制并修改关于 Phone 生产管理的所有代码,显然这是一种很笨拙的办法,对扩展和维护是不利的。
抽象工厂模式会在 AbstarctFactory 里增添创建产品的接口,接着在具体的子工厂里把新加产品的创建给实现了,不过这是以子工厂能够生产该产品为前提的。要是子工厂不支持生产该产品,那么继承的这个接口就可以什么都不做。
结语