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

简介:

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 里增添创建产品的接口,接着在具体的子工厂里把新加产品的创建给实现了,不过这是以子工厂能够生产该产品为前提的。要是子工厂不支持生产该产品,那么继承的这个接口就可以什么都不做。

结语

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

28阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

26阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

26阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

26阅读