天天看點

【設計模式面試】這幾個問題你能回答幾個?

【設計模式面試】這幾個問題你能回答幾個?

【金三銀四】設計模式篇

1.談談你對設計模式的了解

1.首先談設計模式的作用:經驗的傳承,提高了軟體複用的水準,最終達到提高軟體開發效率

設計原則 簡單說明
單一職責 一個類隻負責一項職責
裡氏替換原則 子類可以擴充父類的功能,但不能改變父類原有的功能
依賴倒置原則 要依賴于抽象,不要依賴于具體,核心思想是面向接口程式設計
接口隔離原則 建立單一接口,不要建立龐大臃腫的接口,​

​<br>​

​盡量細化接口,接口中的方法盡量少
迪米特法則 (最少知道原則) 一個對象應該對其他對象保持最少的了解
開閉原則 對擴充開放,對修改關閉

2.設計模式的分類

【設計模式面試】這幾個問題你能回答幾個?

3.建立型模式:都是用來幫助我們建立對象的!

【設計模式面試】這幾個問題你能回答幾個?

4.結構性模式:關注對象和類的組織

【設計模式面試】這幾個問題你能回答幾個?

5.行為型模式:關注系統中對象之間的互相交換,研究系統在運作時對象之間的互相通信和協作,進一步明确對象的職責,共有11中模式

【設計模式面試】這幾個問題你能回答幾個?

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.談談你對工廠模式的了解

  工廠模式的作用是幫助我們建立對象,我們不用自己來建立,根據需要建立的對象的複雜度我們可以把工廠模式分為簡單工廠,工廠方法和抽象工廠。

【設計模式面試】這幾個問題你能回答幾個?

5.1 簡單工廠

  簡單工廠模式又稱為靜态工廠方法,他可以根據不同的參數而傳回不同的執行個體,簡單工廠模式專門定義一個類來負責建立其他類的執行個體,被建立的執行個體通常都具有共同的父類。

JDK中的簡單工廠應用:DataFormat

【設計模式面試】這幾個問題你能回答幾個?

自己寫一個簡單工廠的案例

【設計模式面試】這幾個問題你能回答幾個?
/**
 * 簡單工廠
 */
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 工廠方法

  針對于簡單工廠的短闆,引出了工廠方法模式,定義一個使用者建立對象的接口,讓子類決定執行個體化哪個類,工廠方法使一個類的執行個體化延遲到了其子類中。

【設計模式面試】這幾個問題你能回答幾個?

代碼實作:

/**
 * 工廠方法模式
 */
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 抽象工廠

  上面的兩種方式實作的工廠都是生産同一大類的産品,如果要實作生産不同類型的産品這時我們就可以用抽象工廠模式來實作。

【設計模式面試】這幾個問題你能回答幾個?

代碼實作:

/**
 * 抽象工廠:多個産品族
 */
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對象中,

【設計模式面試】這幾個問題你能回答幾個?

直接參考MyBatis的代碼即可

【設計模式面試】這幾個問題你能回答幾個?

是以建造者模式的作用就是幫助我們解決了複雜對象的建立

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 +
                '}';
    }
}      

輸出結果

【設計模式面試】這幾個問題你能回答幾個?

淺克隆的問題:雖然産生了兩個完全不同的對象,但是被複制的對象的所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用都仍然指向原來的對象。

【設計模式面試】這幾個問題你能回答幾個?

7.2 深度克隆

  被複制對象的所有變量都含有與原來的對象相同的值,除去那些引用其他對象的變量。那些引用其他對象的變量将指向被複制過的新對象,而不再是原有的那些被引用的對象。換言之,深複制把要複制的對象所引用的對象都複制了一遍。

實作的效果是:

【設計模式面試】這幾個問題你能回答幾個?

深度克隆(deep clone)有兩種實作方式,第一種是在淺克隆的基礎上實作,第二種是通過序列化和反序列化實作,我們分别來介紹

方式一:在淺克隆的基礎上實作

/**
     * 實作克隆的方法
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        // 實作深度克隆
        user.birth = (Date) this.birth.clone();
        return user;
    }      
【設計模式面試】這幾個問題你能回答幾個?

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

名稱 說明
序列化 把對象轉換為位元組序列的過程。
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);
}