【设计模式面试】这几个问题你能回答几个?_设计模式

【金三银四】设计模式篇

1.谈谈你对设计模式的理解

1.首先谈设计模式的作用:经验的传承,提高了软件复用的水平,最终达到提高软件开发效率

设计原则

简单说明

单一职责

一个类只负责一项职责

里氏替换原则

子类可以扩展父类的功能,但不能改变父类原有的功能

依赖倒置原则

要依赖于抽象,不要依赖于具体,核心思想是面向接口编程

接口隔离原则

建立单一接口,不要建立庞大臃肿的接口,​​<br>​​尽量细化接口,接口中的方法尽量少

迪米特法则 (最少知道原则)

一个对象应该对其他对象保持最少的了解

开闭原则

对扩展开放,对修改关闭

2.设计模式的分类

【设计模式面试】这几个问题你能回答几个?_Java_02

3.创建型模式:都是用来帮助我们创建对象的!

【设计模式面试】这几个问题你能回答几个?_原型_03

4.结构性模式:关注对象和类的组织

【设计模式面试】这几个问题你能回答几个?_单例_04

5.行为型模式:关注系统中对象之间的相互交换,研究系统在运行时对象之间的相互通信和协作,进一步明确对象的职责,共有11中模式

【设计模式面试】这几个问题你能回答几个?_设计模式_05

2.谈谈你对单例模式的理解

作用:单例模式的核心是保证一个类只有一个实例,并且提供一个访问实例的全局访问点。

实现方式

优缺点

饿汉式

线程安全,调用效率高 ,但是不能延迟加载

懒汉式

线程安全,调用效率不高,能延迟加载

双重检测锁式

在懒汉式的基础上解决并发问题

静态内部类式

线程安全,资源利用率高,可以延时加载

枚举单例

线程安全,调用效率高,但是不能延迟加载

饿汉式

也就是类加载的时候立即实例化对象,实现的步骤是先私有化构造方法,对外提供唯一的静态入口方法,实现如下

/**
* 单例模式:饿汉式
* @author 波波烤鸭
*
*/
public class SingletonInstance1 {
// 声明此类型的变量,并实例化,当该类被加载的时候就完成了实例化并保存在了内存中
private final static SingletonInstance1 instance = new SingletonInstance1();

// 私有化所有的构造方法,防止直接通过new关键字实例化
private SingletonInstance1(){}
// 对外提供一个获取实例的静态方法
public static SingletonInstance1 getInstance(){
return instance;
}
}

饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字

问题:如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!

/**
* 单例模式:饿汉式
* @author 波波烤鸭
*
*/
public class SingletonInstance1 {
private byte[] b1 = new byte[1024*1024];
private byte[] b2 = new byte[1024*1024];
private byte[] b3 = new byte[1024*1024];
// 声明此类型的变量,并实例化,当该类被加载的时候就完成了实例化并保存在了内存中
private final static SingletonInstance1 instance = new SingletonInstance1();

// 私有化所有的构造方法,防止直接通过new关键字实例化
private SingletonInstance1(){}
// 对外提供一个获取实例的静态方法
public static SingletonInstance1 getInstance(){
return instance;
}
}

懒汉式

/**
* 单例模式:懒汉式
* @author 波波烤鸭
*
*/
public class SingletonInstance2 {
// 声明此类型的变量,但没有实例化
private static SingletonInstance2 instance = null;

// 私有化所有的构造方法,防止直接通过new关键字实例化
private SingletonInstance2(){}
// 对外提供一个获取实例的静态方法,为了数据安全添加synchronized关键字
public static synchronized SingletonInstance2 getInstance(){
if(instance == null){
// 当instance不为空的时候才实例化
instance = new SingletonInstance2();
}
return instance;
}
}

  此种方式在类加载后如果我们一直没有调用getInstance方法,那么就不会实例化对象。实现了延迟加载,但是因为在方法上添加了synchronized关键字,每次调用getInstance方法都会同步,所以对性能的影响比较大。

双重检测锁

/**
* 单例模式:懒汉式
* 双重检测机制
* @author 波波烤鸭
*
*/
public class SingletonInstance3 {
// 声明此类型的变量,但没有实例化
private static volatile SingletonInstance3 instance = null;

// 私有化所有的构造方法,防止直接通过new关键字实例化
private SingletonInstance3(){}
// 对外提供一个获取实例的静态方法,
public static SingletonInstance3 getInstance(){
if(instance == null){
synchronized(SingletonInstance3.class){
if(instance == null){
// 1.分配内存空间 2. 执行构造方法,实例化对象 3.把这个对象赋值给这个空间
// 如果不加volatile 会执行重排序 1 3 2
instance = new SingletonInstance3();
}
}
}
return instance;
}
}

不加volatile有指令重排序的问题。添加后可以解决。

静态内部类

/**
* 静态内部类实现方式
* @author 波波烤鸭
*
*/
public class SingletonInstance4 {
// 静态内部类
public static class SingletonClassInstance{
// 声明外部类型的静态常量
public static final SingletonInstance4 instance = new SingletonInstance4();
}
// 私有化构造方法
private SingletonInstance4(){}

// 对外提供的唯一获取实例的方法
public static SingletonInstance4 getInstance(){
return SingletonClassInstance.instance;
}
}

枚举单例

/**
* 单例模式:枚举方式实现
* @author dengp
*
*/
public enum SingletonInstance5 {

// 定义一个枚举元素,则这个元素就代表了SingletonInstance5的实例
INSTANCE;

public void singletonOperation(){
// 功能处理
}
}

3.怎么解决反射爆破单例

  在单例中我们定义的私有的构造器,但是我们知道反射是可以操作私有的属性和方法的,这时我们应该怎么处理?

public static void main(String[] args) throws Exception, IllegalAccessException {
SingletonInstance1 s1 = SingletonInstance1.getInstance();
// 反射方式获取实例
Class c1 = SingletonInstance1.class;
Constructor constructor = c1.getDeclaredConstructor(null);
constructor.setAccessible(true);
SingletonInstance1 s2 = (SingletonInstance1)constructor.newInstance(null);
System.out.println(s1);
System.out.println(s2);
}

输出结果

com.dpb.single.SingletonInstance1@15db9742
com.dpb.single.SingletonInstance1@6d06d69c

产生了两个对象,和单例的设计初衷违背了。
解决的方式是在无参构造方法中手动抛出异常控制,或者声明一个全局变量来控制。

// 私有化所有的构造方法,防止直接通过new关键字实例化
private SingletonInstance2(){
if(instance != null){
// 只能有一个实例存在,如果再次调用该构造方法就抛出异常,防止反射方式实例化
throw new RuntimeException("单例模式只能创建一个对象");
}
}

上面这种方式我们还可以通过反序列化的方式来破解

public static void main(String[] args) throws Exception, IllegalAccessException {
SingletonInstance2 s1 = SingletonInstance2.getInstance();
// 将实例对象序列化到文件中
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("c:/tools/a.txt"));
oos.writeObject(s1);
oos.flush();
oos.close();
// 将实例从文件中反序列化出来
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("c:/tools/a.txt"));
SingletonInstance2 s2 = (SingletonInstance2) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
}

我们只需要在单例类中重写readResolve方法并在该方法中返回单例对象即可,如下:

package com.dpb.single;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
* 单例模式:懒汉式
* @author 波波烤鸭
*
*/
public class SingletonInstance2 implements Serializable{

// 声明此类型的变量,但没有实例化
private static SingletonInstance2 instance = null;

// 私有化所有的构造方法,防止直接通过new关键字实例化
private SingletonInstance2(){
if(instance != null){
// 只能有一个实例存在,如果再次调用该构造方法就抛出异常,防止反射方式实例化
throw new RuntimeException("单例模式只能创建一个对象");
}
}
// 对外提供一个获取实例的静态方法,为了数据安全添加synchronized关键字
public static synchronized SingletonInstance2 getInstance(){
if(instance == null){
// 当instance不为空的时候才实例化
instance = new SingletonInstance2();
}
return instance;
}
// 重写该方法,防止序列化和反序列化获取实例
private Object readResolve() throws ObjectStreamException{
return instance;
}
}

说明:readResolve方法是基于回调的,反序列化时,如果定义了readResolve()则直接返回此方法指定的对象,而不需要在创建新的对象!

4.说说你在哪些框架中看到了单例的设计

1.Spring中的Bean对象,默认是单例模式

2.相关的工厂对象都是单例,比如:MyBatis中的SqlSessionFactory,Spring中的BeanFactory

3.保存相关配置信息的都是单例,比如:MyBatis中的Configuration对象,SpringBoot中的各个XXXAutoConfiguration对象等

4.应用程序的日志应用,一般都会通过单例来实现

5.数据库连接池的设计也是单例模式

5.谈谈你对工厂模式的理解

  工厂模式的作用是帮助我们创建对象,我们不用自己来创建,根据需要创建的对象的复杂度我们可以把工厂模式分为简单工厂,工厂方法和抽象工厂。

【设计模式面试】这几个问题你能回答几个?_工厂_06

5.1 简单工厂

  简单工厂模式又称为静态工厂方法,他可以根据不同的参数而返回不同的实例,简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

JDK中的简单工厂应用:DataFormat

【设计模式面试】这几个问题你能回答几个?_设计模式_07

自己写一个简单工厂的案例

【设计模式面试】这几个问题你能回答几个?_工厂_08

/**
* 简单工厂
*/
public class SimpleFactory {

public static void main(String[] args) {
// 根据对应的类型返回相关产品
CarFactory.createCar("奥迪").run();
CarFactory.createCar("Byd").run();
}
}

// 定义公共的接口
interface Car{
void run();
}

class Audi implements Car{
@Override
public void run() {
System.out.println("奥迪在跑...");
}
}

class Byd implements Car{
@Override
public void run() {
System.out.println("Byd在跑...");
}
}

// 创建对应的简单工厂类
class CarFactory{
public static Car createCar(String type){
if("奥迪".equals(type)){
return new Audi();
}else if("Byd".equals(type)){
return new Byd();
}else{
throw new RuntimeException("该产品不能生产");
}
}
}

我们可以发现简单工厂对于新增产品是无能为力的!不修改原有代码根本就没办法扩展!!!

5.2 工厂方法

  针对于简单工厂的短板,引出了工厂方法模式,定义一个用户创建对象的接口,让子类决定实例化哪个类,工厂方法使一个类的实例化延迟到了其子类中。

【设计模式面试】这几个问题你能回答几个?_单例_09

代码实现:

/**
* 工厂方法模式
*/
public class FactoryMethod {

public static void main(String[] args) {
new AudiCarFactory().createCar().run();
new BydCarFactory().createCar().run();
}


public static interface Car{
public void run();
}

public static class Byd implements Car{
@Override
public void run() {
System.out.println("比亚迪...");
}
}

public static class Audi implements Car{
@Override
public void run() {
System.out.println("奥迪...");
}
}

public static interface CarFactory{
public Car createCar();
}

// 扩展的工厂
public static class AudiCarFactory implements CarFactory{
@Override
public Car createCar() {
return new Audi();
}
}

public static class BydCarFactory implements CarFactory{
@Override
public Car createCar() {
return new Byd();
}
}
}

简单工厂和工厂方法模式的对比

  1. 简单工厂只有一个工厂,而工厂方法有多个工厂
  2. 简单工厂不支持扩展,而工厂方法支持扩展,扩展的方式就是添加对应的工厂类即可
  3. 简单工厂代码复杂度低,工厂方法代码复杂度高

5.3 抽象工厂

  上面的两种方式实现的工厂都是生产同一大类的产品,如果要实现生产不同类型的产品这时我们就可以用抽象工厂模式来实现。

【设计模式面试】这几个问题你能回答几个?_工厂_10

代码实现:

/**
* 抽象工厂:多个产品族
*/
public class AbstractFactory {

public static void main(String[] args) {
Car car = new LuxuryEngineCarFacory().createCar();
Engine engine = new LuxuryEngineCarFacory().createEngine();
car.run();
engine.run();
}

// 抽象工厂
public static interface AbstarctComponentFactory{
Car createCar();
Engine createEngine();
}

public static class LuxuryEngineCarFacory implements AbstarctComponentFactory{
@Override
public Engine createEngine() {
return new LuxuryEngineFactory().createEngine();
}

@Override
public Car createCar() {
return new BydCarFactory().createCar();
}
}

public static class LowEngineCarFacory implements AbstarctComponentFactory{
@Override
public Car createCar() {
return new AudiCarFactory().createCar();
}

@Override
public Engine createEngine() {
return new LowEngineFactory().createEngine();
}
}

// 汽车产品族
public static interface Car{
public void run();
}

public static class Byd implements Car {
@Override
public void run() {
System.out.println("比亚迪...");
}
}

public static class Audi implements Car {
@Override
public void run() {
System.out.println("奥迪...");
}
}

public static interface CarFactory{
public Car createCar();
}

// 扩展的工厂
public static class AudiCarFactory implements CarFactory {
@Override
public Car createCar() {
return new Audi();
}
}

public static class BydCarFactory implements CarFactory{
@Override
public Car createCar() {
return new Byd();
}
}

// 发动机产品族
public static interface Engine{
public void run();
}

public static class LuxuryEngine implements Engine{
@Override
public void run() {
System.out.println("豪华版发动机...");
}
}

public static class LowEngine implements Engine{
@Override
public void run() {
System.out.println("低配版发动机...");
}
}

public static interface EngineFactory{
public Engine createEngine();
}

public static class LuxuryEngineFactory implements EngineFactory{
@Override
public Engine createEngine() {
return new LuxuryEngine();
}
}

public static class LowEngineFactory implements EngineFactory{
@Override
public Engine createEngine() {
return new LowEngine();
}
}
}

三者的对比:

  1. 简单工厂模式(静态工厂模式) :虽然某种程度不符合设计原则,但实际使用最多。
  2. 工厂方法模式:不修改已有类的前提下,通过增加新的工厂类实现扩展。
  3. 抽象工厂模式:不可以增加产品,可以增加产品族!

6.谈谈你对建造者模式的理解

  实际开发中,我们所需要的对象构建时非常复杂,且有很多步骤需要处理时,这时建造者模式就很适合。比如MyBatis中的SqlSessionFactory对象的创建,我们不光要创建SqlSessionFactory本身的对象,还有完成MyBatis的全局配置文件和映射文件的加载解析操作,之后把解析出来的信息绑定在SqlSessionFactory对象中,

【设计模式面试】这几个问题你能回答几个?_工厂_11

直接参考MyBatis的代码即可

【设计模式面试】这几个问题你能回答几个?_单例_12

所以建造者模式的作用就是帮助我们解决了复杂对象的创建

7.谈谈你对原型模式的理解

  在java中我们知道通过new关键字创建的对象是非常繁琐的(类加载判断,内存分配,初始化等),在我们需要大量对象的情况下,原型模式就是我们可以考虑实现的方式。
  原型模式我们也称为克隆模式,即一个某个对象为原型克隆出来一个一模一样的对象,该对象的属性和原型对象一模一样。而且对于原型对象没有任何影响。原型模式的克隆方式有两种:浅克隆和深度克隆.

原型模式

说明

浅克隆

只是拷贝本对象,其对象内部的数组、引用对象等都不拷贝,``还是指向原生对象的内部元素地址

深度克隆

深复制把要复制的对象所引用的对象都复制了一遍

7.1 浅克隆

  被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。 Object类提供的方法clone=只是拷贝本对象= , =其对象内部的数组、引用对象等都不拷贝= ,还是指向原生对象的内部元素地址.

  被克隆的对象必须Cloneable,Serializable这两个接口;

package com.bobo.prototype;

import java.io.Serializable;
import java.util.Date;

public class User implements Cloneable, Serializable {

private String name;

private Date birth;

private int age;

/**
* 实现克隆的方法
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Date getBirth() {
return birth;
}

public void setBirth(Date birth) {
this.birth = birth;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public static void main(String[] args) throws Exception {
// 创建一个普通对象
Date date = new Date(666666);
User user = new User();
user.setName("波波烤鸭");
user.setAge(18);
user.setBirth(date);
System.out.println("原型对象的属性:" + user);
// 克隆对象
User cloneUser = (User) user.clone();
System.out.println("克隆的对象的属性:" + cloneUser);
// 修改原型对象的属性
date.setTime(12345677);
// 修改克隆对象的属性
cloneUser.setName("波哥");
System.out.println("原型对象的属性:" + user);
System.out.println("克隆的对象的属性:" + cloneUser);
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", birth=" + birth +
", age=" + age +
'}';
}
}

输出结果

【设计模式面试】这几个问题你能回答几个?_单例_13

浅克隆的问题:虽然产生了两个完全不同的对象,但是被复制的对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。

【设计模式面试】这几个问题你能回答几个?_设计模式_14

7.2 深度克隆

  被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
实现的效果是:

【设计模式面试】这几个问题你能回答几个?_工厂_15

深度克隆(deep clone)有两种实现方式,第一种是在浅克隆的基础上实现,第二种是通过序列化和反序列化实现,我们分别来介绍

方式一:在浅克隆的基础上实现

/**
* 实现克隆的方法
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
User user = (User) super.clone();
// 实现深度克隆
user.birth = (Date) this.birth.clone();
return user;
}

【设计模式面试】这几个问题你能回答几个?_Java_16

方式二:序列化和反序列化

名称

说明

序列化

把对象转换为字节序列的过程。

反序列化

把字节序列恢复为对象的过程。

public static void main(String[] args) throws CloneNotSupportedException, Exception {
Date date = new Date(1231231231231l);
User user = new User();
user.setName("波波烤鸭");
user.setAge(18);
user.setBirth(date);
System.out.println("-----原型对象的属性------");
System.out.println(user);

//使用序列化和反序列化实现深复制
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(user);
byte[] bytes = bos.toByteArray();

ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);

//克隆好的对象!
User user1 = (User) ois.readObject();

// 修改原型对象的值
date.setTime(221321321321321l);
System.out.println(user.getBirth());

System.out.println("------克隆对象的属性-------");
System.out.println(user1);
}