設計模式常見面試題彙總
1.說一下設計模式?你都知道哪些?
答:設計模式總共有 23 種,總體來說可以分為三大類:建立型模式( Creational Patterns )、結構型模式( Structural Patterns )和行為型模式( Behavioral Patterns )。
**分類** **包含** **關注點** 建立型模式 工廠模式、抽象工廠模式、單例模式、建造者模式、原型模式 關注於物件的建立,同時隱藏建立邏輯 結構型模式 介面卡模式、過濾器模式、裝飾模式、享元模式、代理模式、外觀模式、組合模式、橋接模式 關注類和物件之間的組合 行為型模式 責任鏈模式、命令模式、中介者模式、觀察者模式、狀態模式、策略模式、模板模式、空物件模式、備忘錄模式、迭代器模式、直譯器模式、訪問者模式 關注物件之間的通訊
下面會對常用的設計模式分別做詳細的說明。
2.什麼是單例模式?
答:單例模式是一種常用的軟體設計模式,在應用這個模式時,單例物件的類必須保證只有一個例項存在,整個系統只能使用一個物件例項。
優點:不會頻繁地建立和銷燬物件,浪費系統資源。
使用場景:IO 、資料庫連線、Redis 連線等。
單例模式程式碼實現:
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
單例模式呼叫程式碼:
public class Lesson7\_3 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
程式的輸出結果:true
可以看出以上單例模式是在類載入的時候就建立了,這樣會影響程式的啟動速度,那如何實現單例模式的延遲載入?在使用時再建立?
單例延遲載入程式碼:
// 單例模式-延遲載入版
class SingletonLazy {
private static SingletonLazy instance;
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
以上為非執行緒安全的,單例模式如何支援多執行緒?
使用 synchronized 來保證,單例模式的執行緒安全程式碼:
class SingletonLazy {
private static SingletonLazy instance;
public static synchronized SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
3.什麼是簡單工廠模式?
答:簡單工廠模式又叫靜態工廠方法模式,就是建立一個工廠類,對實現了同一介面的一些類進行例項的建立。比如,一臺咖啡機就可以理解為一個工廠模式,你只需要按下想喝的咖啡品類的按鈕(摩卡或拿鐵),它就會給你生產一杯相應的咖啡,你不需要管它內部的具體實現,只要告訴它你的需求即可。
優點:
- 工廠類含有必要的判斷邏輯,可以決定在什麼時候建立哪一個產品類的例項,客戶端可以免除直接建立產品物件的責任,而僅僅“消費”產品;簡單工廠模式通過這種做法實現了對責任的分割,它提供了專門的工廠類用於建立物件;
- 客戶端無須知道所建立的具體產品類的類名,只需要知道具體產品類所對應的引數即可,對於一些複雜的類名,通過簡單工廠模式可以減少使用者的記憶量;
- 通過引入配置檔案,可以在不修改任何客戶端程式碼的情況下更換和增加新的具體產品類,在一定程度上提高了系統的靈活性。
缺點:
- 不易拓展,一旦新增新的產品型別,就不得不修改工廠的建立邏輯;
- 產品型別較多時,工廠的建立邏輯可能過於複雜,一旦出錯可能造成所有產品的建立失敗,不利於系統的維護。
簡單工廠示意圖如下:
簡單工廠程式碼實現:
class Factory {
public static String createProduct(String product) {
String result = null;
switch (product) {
case "Mocca":
result = "摩卡";
break;
case "Latte":
result = "拿鐵";
break;
default:
result = "其他";
break;
}
return result;
}
}
4.什麼是抽象工廠模式?
答:抽象工廠模式是在簡單工廠的基礎上將未來可能需要修改的程式碼抽象出來,通過繼承的方式讓子類去做決定。
比如,以上面的咖啡工廠為例,某天我的口味突然變了,不想喝咖啡了想喝啤酒,這個時候如果直接修改簡單工廠裡面的程式碼,這種做法不但不夠優雅,也不符合軟體設計的“開閉原則”,因為每次新增品類都要修改原來的程式碼。這個時候就可以使用抽象工廠類了,抽象工廠裡只宣告方法,具體的實現交給子類(子工廠)去實現,這個時候再有新增品類的需求,只需要新建立程式碼即可。
抽象工廠實現程式碼如下:
public class AbstractFactoryTest {
public static void main(String[] args) {
// 抽象工廠
String result = (new CoffeeFactory()).createProduct("Latte");
System.out.println(result); // output:拿鐵
}
}
// 抽象工廠
abstract class AbstractFactory{
public abstract String createProduct(String product);
}
// 啤酒工廠
class BeerFactory extends AbstractFactory{
@Override
public String createProduct(String product) {
String result = null;
switch (product) {
case "Hans":
result = "漢斯";
break;
case "Yanjing":
result = "燕京";
break;
default:
result = "其他啤酒";
break;
}
return result;
}
}
/\* \* 咖啡工廠 \*/
class CoffeeFactory extends AbstractFactory{
@Override
public String createProduct(String product) {
String result = null;
switch (product) {
case "Mocca":
result = "摩卡";
break;
case "Latte":
result = "拿鐵";
break;
default:
result = "其他咖啡";
break;
}
return result;
}
}
5.什麼是觀察者模式?
觀察者模式是定義物件間的一種一對多依賴關係,使得每當一個物件狀態發生改變時,其相關依賴物件皆得到通知並被自動更新。觀察者模式又叫做釋出-訂閱(Publish/Subscribe)模式、模型-檢視(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。優點:
- 觀察者模式可以實現表示層和資料邏輯層的分離,並定義了穩定的訊息更新傳遞機制,抽象了更新介面,使得可以有各種各樣不同的表示層作為具體觀察者角色;
- 觀察者模式在觀察目標和觀察者之間建立一個抽象的耦合;
- 觀察者模式支援廣播通訊;
- 觀察者模式符合開閉原則(對拓展開放,對修改關閉)的要求。
缺點:
- 如果一個觀察目標物件有很多直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間;
- 如果在觀察者和觀察目標之間有迴圈依賴的話,觀察目標會觸發它們之間進行迴圈呼叫,可能導致系統崩潰;
- 觀察者模式沒有相應的機制讓觀察者知道所觀察的目標物件是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。
在觀察者模式中有如下角色:
- Subject:抽象主題(抽象被觀察者),抽象主題角色把所有觀察者物件儲存在一個集合裡,每個主題都可以有任意數量的觀察者,抽象主題提供一個介面,可以增加和刪除觀察者物件;
- ConcreteSubject:具體主題(具體被觀察者),該角色將有關狀態存入具體觀察者物件,在具體主題的內部狀態發生改變時,給所有註冊過的觀察者傳送通知;
- Observer:抽象觀察者,是觀察者者的抽象類,它定義了一個更新介面,使得在得到主題更改通知時更新自己;
- ConcrereObserver:具體觀察者,實現抽象觀察者定義的更新介面,以便在得到主題更改通知時更新自身的狀態。
觀察者模式實現程式碼如下。
1)定義觀察者(訊息接收方)
/\* \* 觀察者(訊息接收方) \*/
interface Observer {
public void update(String message);
}
/\* \* 具體的觀察者(訊息接收方) \*/
class ConcrereObserver implements Observer {
private String name;
public ConcrereObserver(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + ":" + message);
}
}
2)定義被觀察者(訊息傳送方)
/\* \* 被觀察者(訊息釋出方) \*/
interface Subject {
// 增加訂閱者
public void attach(Observer observer);
// 刪除訂閱者
public void detach(Observer observer);
// 通知訂閱者更新訊息
public void notify(String message);
}
/\* \* 具體被觀察者(訊息釋出方) \*/
class ConcreteSubject implements Subject {
// 訂閱者列表(儲存資訊)
private List<Observer> list = new ArrayList<Observer>();
@Override
public void attach(Observer observer) {
list.add(observer);
}
@Override
public void detach(Observer observer) {
list.remove(observer);
}
@Override
public void notify(String message) {
for (Observer observer : list) {
observer.update(message);
}
}
}
3)程式碼呼叫
public class ObserverTest {
public static void main(String[] args) {
// 定義釋出者
ConcreteSubject concreteSubject = new ConcreteSubject();
// 定義訂閱者
ConcrereObserver concrereObserver = new ConcrereObserver("老王");
ConcrereObserver concrereObserver2 = new ConcrereObserver("Java");
// 新增訂閱
concreteSubject.attach(concrereObserver);
concreteSubject.attach(concrereObserver2);
// 釋出資訊
concreteSubject.notify("更新了");
}
}
程式執行結果如下:
老王:更新了
Java:更新了
6.什麼是裝飾器模式?
答:裝飾器模式是指動態地給一個物件增加一些額外的功能,同時又不改變其結構。
優點:裝飾類和被裝飾類可以獨立發展,不會相互耦合,裝飾模式是繼承的一個替代模式,裝飾模式可以動態擴充套件一個實現類的功能。
裝飾器模式的關鍵:裝飾器中使用了被裝飾的物件。
比如,建立一個物件“laowang”,給物件新增不同的裝飾,穿上夾克、戴上帽子......,這個執行過程就是裝飾者模式,實現程式碼如下。
1)定義頂層物件,定義行為
interface IPerson {
void show();
}
2)定義裝飾器超類
class DecoratorBase implements IPerson{
IPerson iPerson;
public DecoratorBase(IPerson iPerson){
this.iPerson = iPerson;
}
@Override
public void show() {
iPerson.show();
}
}
3)定義具體裝飾器
class Jacket extends DecoratorBase {
public Jacket(IPerson iPerson) {
super(iPerson);
}
@Override
public void show() {
// 執行已有功能
iPerson.show();
// 定義新行為
System.out.println("穿上夾克");
}
}
class Hat extends DecoratorBase {
public Hat(IPerson iPerson) {
super(iPerson);
}
@Override
public void show() {
// 執行已有功能
iPerson.show();
// 定義新行為
System.out.println("戴上帽子");
}
}
4)定義具體物件
class LaoWang implements IPerson{
@Override
public void show() {
System.out.println("什麼都沒穿");
}
}
5)裝飾器模式呼叫
public class DecoratorTest {
public static void main(String[] args) {
LaoWang laoWang = new LaoWang();
Jacket jacket = new Jacket(laoWang);
Hat hat = new Hat(jacket);
hat.show();
}
}
7.什麼是模板方法模式?
答:模板方法模式是指定義一個模板結構,將具體內容延遲到子類去實現。
優點:
- 提高程式碼複用性:將相同部分的程式碼放在抽象的父類中,而將不同的程式碼放入不同的子類中;
- 實現了反向控制:通過一個父類呼叫其子類的操作,通過對子類的具體實現擴充套件不同的行為,實現了反向控制並且符合開閉原則。
以給冰箱中放水果為例,比如,我要放一個香蕉:開冰箱門 → 放香蕉 → 關冰箱門;如果我再要放一個蘋果:開冰箱門 → 放蘋果 → 關冰箱門。可以看出它們之間的行為模式都是一樣的,只是存放的水果品類不同而已,這個時候就非常適用模板方法模式來解決這個問題,實現程式碼如下:
/\* \* 新增模板方法 \*/
abstract class Refrigerator {
public void open() {
System.out.println("開冰箱門");
}
public abstract void put();
public void close() {
System.out.println("關冰箱門");
}
}
class Banana extends Refrigerator {
@Override
public void put() {
System.out.println("放香蕉");
}
}
class Apple extends Refrigerator {
@Override
public void put() {
System.out.println("放蘋果");
}
}
/\* \* 呼叫模板方法 \*/
public class TemplateTest {
public static void main(String[] args) {
Refrigerator refrigerator = new Banana();
refrigerator.open();
refrigerator.put();
refrigerator.close();
}
}
程式執行結果:
開冰箱門
放香蕉
關冰箱門
8.什麼是代理模式?
代理模式是給某一個物件提供一個代理,並由代理物件控制對原物件的引用。
優點:
- 代理模式能夠協調呼叫者和被呼叫者,在一定程度上降低了系統的耦合度;
- 可以靈活地隱藏被代理物件的部分功能和服務,也增加額外的功能和服務。
缺點:
- 由於使用了代理模式,因此程式的效能沒有直接呼叫效能高;
- 使用代理模式提高了程式碼的複雜度。
舉一個生活中的例子:比如買飛機票,由於離飛機場太遠,直接去飛機場買票不太現實,這個時候我們就可以上攜程 App 上購買飛機票,這個時候攜程 App 就相當於是飛機票的代理商。
代理模式實現程式碼如下:
/\* \* 定義售票介面 \*/
interface IAirTicket {
void buy();
}
/\* \* 定義飛機場售票 \*/
class AirTicket implements IAirTicket {
@Override
public void buy() {
System.out.println("買票");
}
}
/\* \* 代理售票平臺 \*/
class ProxyAirTicket implements IAirTicket {
private AirTicket airTicket;
public ProxyAirTicket() {
airTicket = new AirTicket();
}
@Override
public void buy() {
airTicket.buy();
}
}
/\* \* 代理模式呼叫 \*/
public class ProxyTest {
public static void main(String[] args) {
IAirTicket airTicket = new ProxyAirTicket();
airTicket.buy();
}
}
9.什麼是策略模式?
答:策略模式是指定義一系列演算法,將每個演算法都封裝起來,並且使他們之間可以相互替換。
優點:遵循了開閉原則,擴充套件性良好。
缺點:隨著策略的增加,對外暴露越來越多。
以生活中的例子來說,比如我們要出去旅遊,選擇性很多,可以選擇騎車、開車、坐飛機、坐火車等,就可以使用策略模式,把每種出行作為一種策略封裝起來,後面增加了新的交通方式了,如超級高鐵、火箭等,就可以不需要改動原有的類,新增交通方式即可,這樣也符合軟體開發的開閉原則。 策略模式實現程式碼如下:
/\* \* 宣告旅行 \*/
interface ITrip {
void going();
}
class Bike implements ITrip {
@Override
public void going() {
System.out.println("騎自行車");
}
}
class Drive implements ITrip {
@Override
public void going() {
System.out.println("開車");
}
}
/\* \* 定義出行類 \*/
class Trip {
private ITrip trip;
public Trip(ITrip trip) {
this.trip = trip;
}
public void doTrip() {
this.trip.going();
}
}
/\* \* 執行方法 \*/
public class StrategyTest {
public static void main(String[] args) {
Trip trip = new Trip(new Bike());
trip.doTrip();
}
}
程式執行的結果:
騎自行車
10.什麼是介面卡模式?
答:介面卡模式是將一個類的介面變成客戶端所期望的另一種介面,從而使原本因介面不匹配而無法一起工作的兩個類能夠在一起工作。
優點:
- 可以讓兩個沒有關聯的類一起執行,起著中間轉換的作用;
- 靈活性好,不會破壞原有的系統。
缺點:過多地使用介面卡,容易使程式碼結構混亂,如明明看到呼叫的是 A 介面,內部呼叫的卻是 B 介面的實現。
以生活中的例子來說,比如有一個充電器是 MicroUSB 介面,而手機充電口卻是 TypeC 的,這個時候就需要一個把 MicroUSB 轉換成 TypeC 的介面卡,如下圖所示:
介面卡實現程式碼如下:
/\* \* 傳統的充電線 MicroUSB \*/
interface MicroUSB {
void charger();
}
/\* \* TypeC 充電口 \*/
interface ITypeC {
void charger();
}
class TypeC implements ITypeC {
@Override
public void charger() {
System.out.println("TypeC 充電");
}
}
/\* \* 介面卡 \*/
class AdapterMicroUSB implements MicroUSB {
private TypeC typeC;
public AdapterMicroUSB(TypeC typeC) {
this.typeC = typeC;
}
@Override
public void charger() {
typeC.charger();
}
}
/\* \* 測試呼叫 \*/
public class AdapterTest {
public static void main(String[] args) {
TypeC typeC = new TypeC();
MicroUSB microUSB = new AdapterMicroUSB(typeC);
microUSB.charger();
}
}
程式執行結果:
TypeC 充電
11.JDK 類庫常用的設計模式有哪些?
答:JDK 常用的設計模式如下:
1)工廠模式
java.text.DateFormat 工具類,它用於格式化一個本地日期或者時間。
public final static DateFormat getDateInstance();
public final static DateFormat getDateInstance(int style);
public final static DateFormat getDateInstance(int style,Locale locale);
加密類
KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede");
Cipher cipher = Cipher.getInstance("DESede");
2)介面卡模式
把其他類適配為集合類
List<Integer> arrayList = java.util.Arrays.asList(new Integer[]{1,2,3});
List<Integer> arrayList = java.util.Arrays.asList(1,2,3);
3)代理模式
如 JDK 本身的動態代理。
interface Animal {
void eat();
}
class Dog implements Animal {
@Override
public void eat() {
System.out.println("The dog is eating");
}
}
class Cat implements Animal {
@Override
public void eat() {
System.out.println("The cat is eating");
}
}
// JDK 代理類
class AnimalProxy implements InvocationHandler {
private Object target; // 代理物件
public Object getInstance(Object target) {
this.target = target;
// 取得代理物件
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("呼叫前");
Object result = method.invoke(target, args); // 方法呼叫
System.out.println("呼叫後");
return result;
}
}
public static void main(String[] args) {
// JDK 動態代理呼叫
AnimalProxy proxy = new AnimalProxy();
Animal dogProxy = (Animal) proxy.getInstance(new Dog());
dogProxy.eat();
}
4)單例模式
全域性只允許有一個例項,比如:
Runtime.getRuntime();
5)裝飾器
為一個物件動態的加上一系列的動作,而不需要因為這些動作的不同而產生大量的繼承類。
java.io.BufferedInputStream(InputStream);
java.io.DataInputStream(InputStream);
java.io.BufferedOutputStream(OutputStream);
java.util.zip.ZipOutputStream(OutputStream);
java.util.Collections.checkedList(List list, Class type) ;
6)模板方法模式
定義一個操作中演算法的骨架,將一些步驟的執行延遲到其子類中。
比如,Arrays.sort() 方法,它要求物件實現 Comparable 介面。
class Person implements Comparable{
private Integer age;
public Person(Integer age){
this.age = age;
}
@Override
public int compareTo(Object o) {
Person person = (Person)o;
return this.age.compareTo(person.age);
}
}
public class SortTest(){
public static void main(String[] args){
Person p1 = new Person(10);
Person p2 = new Person(5);
Person p3 = new Person(15);
Person[] persons = {p1,p2,p3};
//排序
Arrays.sort(persons);
}
}
12.IO 使用了什麼設計模式?
答:IO 使用了介面卡模式和裝飾器模式。
- 介面卡模式:由於 InputStream 是位元組流不能享受到字元流讀取字元那麼便捷的功能,藉助 InputStreamReader 將其轉為 Reader 子類,因而可以擁有便捷操作文字檔案方法;
- 裝飾器模式:將 InputStream 位元組流包裝為其他流的過程就是裝飾器模式,比如,包裝為 FileInputStream、ByteArrayInputStream、PipedInputStream 等。
13.Spring 中都使用了哪些設計模式?
答:Spring 框架使用的設計模式如下。
- 代理模式:在 AOP 中有使用
- 單例模式:bean 預設是單例模式
- 模板方法模式:jdbcTemplate
- 工廠模式:BeanFactory
- 觀察者模式:Spring 事件驅動模型就是觀察者模式很經典的一個應用,比如,ContextStartedEvent 就是 ApplicationContext 啟動後觸發的事件
- 介面卡模式:Spring MVC 中也是用到了介面卡模式適配 Controller