主頁 > 後端開發 > 談談Java常用類別庫中的設計模式 - Part Ⅱ

談談Java常用類別庫中的設計模式 - Part Ⅱ

2020-10-08 23:29:32 後端開發

概述

本系列上一篇:建造者、工廠方法、享元、橋接

本文介紹的設計模式(建議按順序閱讀):

配接器
模板方法
裝飾器

相關縮寫:EJ - Effective Java

Here We Go

配接器 (Adapter)

定義:將一個類的介面轉換成客戶希望的另外一個介面,配接器模式使得原本由于介面不兼容而不能一起作業的那些類可以一起作業,

場景:想使用現有的類,但此類的介面不符合已有系統的需要,同時雙方不太容易修改;通過介面轉換,將一個類插入到另一個類系中,

型別:結構型

配接器聽起來像是一種亡羊補牢,仿佛使用了它就代表你承認了系統設計糟糕、不易擴展,所以才需要在兩個類系之間增加中間者實作兼容,
但配接器真正的靈魂所在,是為一個事物提供多種 視角(perspective)

雖然 HashMap 快被講爛了,但并不妨礙我們以 Design Pattern 的角度來欣賞 HashMapMap::keySet 的實作細節,

    /**
     * Returns a {@link Set} view of the keys contained in this map.
     * The set is backed by the map, so changes to the map are
     * reflected in the set, and vice-versa.  If the map is modified
     * while an iteration over the set is in progress (except through
     * the iterator's own <tt>remove</tt> operation), the results of
     * the iteration are undefined.  The set supports element removal,
     * which removes the corresponding mapping from the map, via the
     * <tt>Iterator.remove</tt>, <tt>Set.remove</tt>,
     * <tt>removeAll</tt>, <tt>retainAll</tt>, and <tt>clear</tt>
     * operations.  It does not support the <tt>add</tt> or <tt>addAll</tt>
     * operations.
     *
     * @return a set view of the keys contained in this map
     */
    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

    final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<K> iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
    }
        
    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }
        

Map::keySet 是配接器模式的典型適用場景: HashMap 實作了 Map 介面,其與標準集合介面 Set 在繼承層次(Map與Set)和資料結構(異構容器與同構容器)上大相徑庭,它們代表著兩個類系,

ketSet() 的職責是將鍵值對中的鍵抽出,組成一個 Set實體,

構造一個HashSet?回圈add?

我們來看看 HashMap 是如何實作這一需求的:

觀察代碼可以發現,keySet() 本身邏輯十分簡單,創建一個內部類 KeySet 的實體,并對其進行實體控制,

再來看看 KeySet 類的邏輯:一個繼承自 AbstractSet的內部類, AbstractSet 是實作了 Set 標準的骨架實作類 ,繼承它之后 KeySet 類只需實作剩下的基本型別介面就可稱自己是一個 Set 了,那么這些介面是如何實作的呢?

size() -> 回傳外部類的 size 欄位,即鍵值對個數,
clear() -> 呼叫外部類 clear() 方法,即清空鍵值對陣列,
iterator() -> 回傳內部類 KeyIterator 實體,此類繼承自通用迭代器 HashIterator ,重寫 next() 回傳下一元素的 key 欄位,
contains() -> 呼叫外部類 containsKey() 方法,
remove() -> 呼叫外部類輔助方法 removeNode() ,即洗掉鍵值對,

針對 Set 所要求的介面能力, HashMap 最大限度地復用已有邏輯,在保持資料正確的前提下,將兩個介面的職責建立映射,

再來看看上述代碼片段中 keySet() 的JavaDoc注釋,

Returns a {@link Set} view of the keys contained in this map.
The set is backed by the map, so changes to the map are
reflected in the set, and vice-versa.

回傳此map中包含的鍵的set視圖,這個set是由map支撐的,所以對map的修改都會反映到set上,反之亦然,

不僅是 keySet()values()entrySet() 都使用了相同的配接器模式,這些配接器方法避免了為適應新標準而重新生成資料結構造成的浪費,

配接器的思路,就是對同一個物件建立多個視角,每一種視角下其特征、行為都不同,從而以更多維度來服務系統,

正所謂——

橫看成嶺側成峰,遠近高低各不同,
不識廬山真面目,只緣身在此山中,




模板方法 (Template Method)

定義:定義一個操作中的演算法的骨架,而將一些步驟延遲到子類中,模板方法使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟,

場景:多個子類共有邏輯相同的方法;重要的、復雜的方法

型別:行為型

在上文配接器的介紹中參考了 HashMap的實作,其中介紹 KeySet 時提及到了 骨架實作類
的概念,而骨架實作類恰恰是模板模式的一種實踐,本節以此為例,

首先復習一下Java的集合框架

集合框架

藍色部分是我們熟知的各種集合實作,它們都繼承自亮綠色部分、以Abstract開頭命名的抽象類,這些類便稱為骨架實作類,它們直接實作了 List SetMap等介面,

摘取一段EJ中關于骨架實作類的描述,

通過對介面提供一個抽象的骨架實作(skeletal implementation)類,可以把介面和抽象類的優點結合起來,介面負責定義型別,或許還提供一些預設方法,而骨架實作類則負責實作除基本型別介面方法之外,剩下的非基本型別介面方法,擴展骨架實作占了實作介面之外的大部分作業,

什么是基本型別介面方法呢?對于這個冗長的命名,我理解就好比Java中萬物皆物件,但所有物件最終的狀態都要由基本型別來表示,組合物件也可看作是被封裝好的基本型別之間進行組合,

換句話說,非基本型別介面方法可以憑借基本型別介面方法推匯出自身的邏輯,這一點和介面的預設方法十分相似,

比如這是List中排序介面的預設方法,

default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

這樣,List的實作類只要保證toArray()listIterator()這些基本型別介面方法行為正確,排序方法就隱式地被實作了,

再摘取AbstractList中的片段,

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

    abstract public E get(int index);

    public List<E> subList(int fromIndex, int toIndex) {
        return (this instanceof RandomAccess ?
                new RandomAccessSubList<>(this, fromIndex, toIndex) :
                new SubList<>(this, fromIndex, toIndex));
    }
    
    class SubList<E> extends AbstractList<E> {...}
    
    class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {...}
}

AbstractList中的實作更趨于完整,已經通過撰寫輔助內部類將迭代器、子串列等功能進行了實作,

介面-預設方法骨架實作類-非基本型別介面方法 都是可根據其他方法推演自身邏輯的方法,那它們之間的區別在哪呢?不如把骨架實作類中的代碼搬到介面當中!然而這樣是不妥的,它們之間還是有區別的,

骨架實作類為抽象類提供了實作上的幫助,但又不強加“抽象類被用作型別定義時”所特有的嚴格限制, 如果預置的類無法擴展骨架實作類,這個類始終都可以手工實作這個介面,同時仍然受益于介面的預設方法,

介面定義了整個類系的型別,預設方法是針對這一批型別的通解;而骨架實作類是介面的某一種實作方案,它趨于完整,方便最終實作類的撰寫,但不一定是最佳方案,所以不能系結到整個類系之上,

無論是預設方法,還是骨架實作類,都是模板方法的實踐,通過定義模板、繼承模板,可以讓開發者專注于關鍵邏輯,同時也能隨意覆寫模板,讓子類實作高效又靈活,




裝飾器 (Decorator)

定義:動態地將一個物件添加一些額外的職責,就添加功能來說,裝飾模式比生成子類更為靈活,

場景:在不想增加很多子類的情況下擴展類;動態增加功能,動態撤銷,

型別:結構型

復合優先于繼承,這是EJ中提到裝飾器時的Tip標題,它很好的表達了裝飾器出現的原因,
繼承是實作代碼重用的強大工具,但并非總是最佳工具,其中一個原因是:繼承破壞了封裝性

換句話說,子類依賴于其超類中特定功能的實作細節,超類的實作有可能會隨著發行版本的不同而有所變化,如果真的發生了變化,子類可能會遭到破壞,即使它的代碼完全沒有改變,

這里所講的變化可以是以下任意一種:

  • 父類的方法在類內互相呼叫,這種 自用性 (self-use) 是實作細節,開發者可能會認為方法之間是獨立的,如果覆寫某個被依賴的方法,依賴方也會受影響,并且在未來的發行版本中這種依賴關系是變化的、不穩定的,
  • 子類對所有方法加入了一種先決條件,例如驗參,父類如果在后續的發行版本添加新的方法,就會成為“漏網之魚”,造成安全問題,
  • 在新的發行版本中父類撰寫了一個新方法,恰好與某個子類的新增方法簽名沖突,造成編譯失敗,
  • ···

總而言之,倘若不是專門為了繼承而設計并且具有很好的檔案說明的類,在多人協作,特別是跨越包邊界時(泛指不再對子類撰寫、迭代的規范有強約束力)使用繼承非常危險,會讓系統變得更加脆弱,
所幸有一種方法可以避免繼承的種種問題,即 復合-轉發

不擴展現有的類,而是在新的類中增加私有域,參考現有類的一個實體,這種設計被稱為 “復合”(composition)
新類中每個實體方法都可以呼叫被包含的現有類實體中對應的方法,并回傳他的結果,這被稱為 “轉發”(forwarding)

我們還是來看集合框架中的一個典型例子(今天跟集合框架杠上了...

public class Collections {

    public static <K, V> Map<K, V> checkedMap(Map<K, V> m,
                                              Class<K> keyType,
                                              Class<V> valueType) {
        return new CheckedMap<>(m, keyType, valueType);
    }
    
    private static class CheckedMap<K,V> implements Map<K,V>, Serializable {

        private final Map<K, V> m;
        final Class<K> keyType;
        final Class<V> valueType;
        
        CheckedMap(Map<K, V> m, Class<K> keyType, Class<V> valueType) {
            this.m = Objects.requireNonNull(m);
            this.keyType = Objects.requireNonNull(keyType);
            this.valueType = Objects.requireNonNull(valueType);
        }
        
        public int size()                      { return m.size(); }
        public boolean isEmpty()               { return m.isEmpty(); }
        public boolean containsKey(Object key) { return m.containsKey(key); }
        public boolean containsValue(Object v) { return m.containsValue(v); }
        public V get(Object key)               { return m.get(key); }
        public V remove(Object key)            { return m.remove(key); }
        public void clear()                    { m.clear(); }
        public Set<K> keySet()                 { return m.keySet(); }
        public Collection<V> values()          { return m.values(); }
        public boolean equals(Object o)        { return o == this || m.equals(o); }
        public int hashCode()                  { return m.hashCode(); }
        public String toString()               { return m.toString(); }

        public V put(K key, V value) {
            typeCheck(key, value);
            return m.put(key, value);
        }
        
        private void typeCheck(Object key, Object value) {
            if (key != null && !keyType.isInstance(key))
                throw new ClassCastException(badKeyMsg(key));

            if (value != null && !valueType.isInstance(value))
                throw new ClassCastException(badValueMsg(value));
        }
        
        //省略剩余方法
    }    
}

這回介紹的是Collections::checkedMap,這個靜態工廠使用不多,其作用是為map實體提供鍵值對型別檢查,現如今Map介面已是一個泛型,但在JAVA SE5之前撰寫的各類map實作,是沒有型別檢查的能力的,當我們不能去改造老的類別庫時,只需一句簡單的呼叫:

Map<Integer, String> typeSafeMap = Collections.checkedMap(new OldMap(), Integer.class, String.class);

即可為老類別庫賦予和泛型一樣的型別檢查能力,我們來細品代碼,

靜態工廠中回傳了 CheckedMap 的新實體, CheckedMap 是實作了 Map 介面的內部類,定義了私有變數 Map 用于接收map實體;兩個 Class 欄位,分別保存鍵值的型別,對于大多數方法, CheckedMap 直接將呼叫 轉發 至原map上,但在 put 這樣的插入操作中,在轉發前呼叫了私有的 typeCheck 方法,執行型別檢查

避開繼承,使用裝飾器,我們同樣能為現有類追加新的功能,同時裝飾器本身還可以再次被裝飾,這使得裝飾器是動態的、可拆卸的,
例如對現有類同時賦予 型別安全執行緒安全 的特性,

        Map<Integer, String> safeMap = Collections.synchronizedMap(
                Collections.checkedMap(new OldMap(), Integer.class, String.class)
        );

裝飾器本身也有撰寫成本,因為需要將所有方法進行轉發,但往往需要裝飾的方法較少, Guava 做了這方面考慮,在collect包下為所有集合介面撰寫了轉發類,類名格式:ForwardingXXX,開發者只需繼承這些轉發類,重寫需要裝飾的方法即可,




參考:

[1] Effective Java - 機械工業出版社 - Joshua Bloch (2017/11)

[2] 《大話設計模式》 - 清華大學出版社 - 陳杰 (2007/12)

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/163612.html

標籤:Java

上一篇:學習筆記之Stream流

下一篇:阿里 fastjson 簡單使用.

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more