BlackFlame33

BlackFlame33

若无力驾驭,自由便是负担。个人博客 https://blackflame33.cn/

《On Java》閱讀筆記-03

第 10 章 介面#

本章介紹了抽象類和介面的定義,使用,以及區別和意義。這章進一步闡釋了 “使用與功能實現分離” 的優秀設計,還順帶介紹了一些設計模式。圖靈導讀目標如圖所示:

第 10 章 介面

介面,到底代表了什麼?#

介面描述了一個類應該是什麼樣的和可以做什麼,但是不包括怎麼做。它實際上是定義了一種類之間的 “協議”。我個人認為介面是一種更高階的抽象:在代碼中對一些相同行為的類進行了進一步的抽象,這帶來了代碼重用和很強的擴展性,但也帶來了額外的複雜性。從這裡可以看出,要保持合理設計,就要適當封裝,將抽象控制在合理的層次,否則這是一種過度設計!

介面的默認方法#

介面本來是不能實現任何方法的,它僅僅代表 “協議”,需要交給子類實現(implements)。但JDK 8依然為介面提供了默認方法。當一個介面有默認方法時,子類可以不用重寫這個方法,以作為默認實現。一種說法是默認方法增強了代碼的兼容性和靈活性。《On Java》認為:這允許向現有介面中添加方法,而不會破壞已經在使用該介面的所有代碼。默認方法應該是為JDK 8引入流的解決方案。

抽象類與介面的區別#

特性介面抽象類
組合可以在新類中組合多個介面只能繼承一個抽象類
狀態(字段)不能包含字段(靜態字段除外,但它們不支持對象狀態)可以包含字段,非抽象方法可以引用這些字段
默認方法與抽象方法默認方法不用被子類實現,它只能引用介面內的方法(字段不行)抽象方法必須在子類實現
構造器不能有構造器可以有構造器
存取權限限制隱式的 public可以為protected或包存取權限

使用準則:“在合理的範圍內儘可能抽象”。所以一般是優先用介面,等需要使用抽象類的時候你自然會知道。但除非必要,否則兩個都不要使用。在後面會講到,這是一種過早優化。介面和抽象類可以作為我們重構代碼的強力工具。

完全解耦#

比起抽象類,介面更受人喜歡些,畢竟如果你調用的方法傳入抽象類及其子類,你將只能使用基類及其子類,不能跳出其繼承體系。而使用介面不受繼承體系限制,能編寫更多可重用的代碼。

淺談策略設計模式#

首先,有關策略設計模式,這篇文章非常好,後續如果有設計模式相關問題我也會引用。

策略設計模式就是創建一個方法,它能根據傳遞的參數對象來表現出不同的行為。這使得上下文沒有必要知道它這個策略是怎麼實現的,我直接用就行了,後續如果有新策略,不用更改上下文,使得方法更加靈活,通用,可重用性也更高。也就是 “我這個方法可以操作任何對象,只要這個對象遵循我的介面”。這也是 “將變化的事物與不變的事物分開” 的設計體現。

這裡寫上Java的簡單的實現,後續可以展開講講:

import java.util.Random;

enum concreteStrategy {
    // 加法
    addition,
    // 減法
    subtraction,
    // 乘法
    multiplication

}

/**
 * 策略介面聲明了某個算法各個不同版本間所共有的操作。上下文會使用該介面來
 * 調用有具體策略定義的算法。
 */
interface Strategy {
    /**
     * 該方法可實現某種二元運算,並返回值。
     *
     * @param num1 運算數 1
     * @param num2 運算數 2
     * @return double 進行運算後的結果
     * @author BlackFlame33
     */
    double execute(int num1, int num2);
}

/**
 * 具體策略會在遵循策略基礎介面的情況下實現算法。該介面實現了它們在上下文
 * 中的互換性。
 */
class ConcreteStrategyAdd implements Strategy {

    @Override
    public double execute(int num1, int num2) {
        return num1 + num2;
    }
}

class ConcreteStrategySubtract implements Strategy {
    @Override
    public double execute(int num1, int num2) {
        return num1 - num2;
    }
}

class ConcreteStrategyMultiply implements Strategy {

    @Override
    public double execute(int num1, int num2) {
        return num1 * num2;
    }
}

/**
 * 上下文定義了客戶端關注的介面。
 */
class Context {
    /**
     * 上下文會維護指向某個策略對象的引用。上下文不知曉策略的具體類。上下
     * 文必須通過策略介面來與所有策略進行交互。
     */
    private Strategy strategy;

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    /**
     * 上下文會將一些工作委派給策略對象,而不是自行實現不同版本的算法。
     *
     * @param num1 運算數 1
     * @param num2 運算數 2
     * @return double 運算結果
     */
    public double executeStrategy(int num1, int num2) {
        return strategy.execute(num1, num2);
    }
}

/**
 * 客戶端代碼會選擇具體策略並將其傳遞給上下文。客戶端必須知曉策略之間的差
 * 異,才能做出正確的選擇。
 *
 * @author BlackFlame33
 */
public class ExampleApplication {

    public static void main(String[] args) {
        // 創建上下文對象。
        Context context = new Context();
        // 讀取第一個數。
        int num1 = new Random().nextInt(10);
        // 讀取最後一個數。
        int num2 = new Random().nextInt(10);
        // 應用會知道應該執行的具體策略
        concreteStrategy action = randomStrategy();
        if (action == concreteStrategy.addition) {
            context.setStrategy(new ConcreteStrategyAdd());
        }
        if (action == concreteStrategy.subtraction) {
            context.setStrategy(new ConcreteStrategySubtract());
        }
        if (action == concreteStrategy.multiplication) {
            context.setStrategy(new ConcreteStrategyMultiply());
        }
        double result = context.executeStrategy(num1, num2);
        // 打印結果
        System.out.println(num1 + " 與 " + num2 + " 的 " + action + " 等於 " + result);
    }

    private static concreteStrategy randomStrategy() {
        return concreteStrategy.values()[new Random().nextInt(concreteStrategy.values().length)];
    }
}

組合介面時的名稱衝突#

將介面組合在一起時,在不同的介面中使用相同的方法名稱通常會導致代碼可讀性變差。(不要這樣命名。。。)

適配介面#

適配器模式#

一樣,看這篇文章就行。

個人理解:適配器就好像在不兼容的對象之間的一層兼容層一樣。有時候不兼容的代碼我們沒有權限修改,這時候我們通過適配器內的方法,將參數適配到合適的格式,然後調用定向到其封裝對象中的一個或多個方法。

如此以來,一個把介面作為參數的方法,幾乎可以讓任何類適應它,只要實現介面的方法就行。這讓我深刻感受到介面的威力。

介面的字段#

抽象類與介面的區別我們了解到介面只有靜態字段。其靜態字段存儲在介面的靜態存儲區中。

嵌套介面#

嵌套介面

淺談工廠模式#

工廠模式將對創建對象的代碼與使用對象的代碼分離,成為其間接層,使得我們在添加新種類的對象時可以直接添加新種類的類,而不用修改客戶端代碼。通常發生在複雜的創建對象的情況時。

更多信息還是看這篇文章

介面,用起來?#

指導方針:“優先使用類而不是介面”。從類開始設計,如果明顯感覺介面是必要,那麼就重構吧!

第 11 章 內部類#

定義在另一個類中的類稱為內部類。

除了內部類的相關語法外,匿名內部類在後續會非常實用,應該熟練掌握。

第 11 章

內部類到外部類的鏈接#

內部類會隱含一個指向外部類對象的引用。因此內部類擁有訪問外部類所有元素的訪問權。也是因此,創建內部類時,為了讓內部類持有外部類對象的引用,必須先創建外部類對象,再通過外部類對象創建內部類。

匿名內部類#

public Contents contents() {
    return new Contents() {
        int i = 11;
    };
}

這種聲明類的方式初看起來比較怪:我正寫到return的時候,要返回一個Contents對象了,哦~等等,讓我先定義一個Contents的子類。但當類僅需要使用一次或極少重複使用時,也沒有必要專門為它寫一個類。

內部類的價值#

內部類的價值體現在:每個內部類可以獨立地繼承類或者實現介面,這完善了多重繼承問題的解決方案。

內部類還可以實現閉包。它能保留它被創建時所處作用域的信息。

第 12 章 集合#

如果對象的數量是固定的,而且這些對象的生命周期都是已知的,那麼這樣的程序是相當簡單的。

數組非常實用,但是其大小長度是已經固定了的,這會帶來很大的局限性,因為通常情況下我們並不能準確知道需要多少個對象。

集合

Iterator#

迭代器可以實現向任意類型的集合的正向遍歷。它讓程序員不用關心所處理的集合的類型(我們說不用關心,其實就是在說,我們不必考慮傳入集合的類型而被迫做不同的處理),它統一了對集合的訪問。

糟糕的設計 ——Stack#

儘管Java提供了Stack類,但這個類的設計非常糟糕(這很可能與其不合理的繼承關係有關),所以JDK 6加入了ArrayDeque,提供了直接使用棧的方法。
警鐘長鳴,謹慎考慮繼承關係!

面向介面#

通過面向介面而不是面向實現來編寫代碼,可以讓我們的代碼可以應用於更多對象類型。

總結#

Java提供了很多持有對象的方式。最重要的四大類:ListMapSetQueue:

  1. Collection保存單個元素,Map保存鍵值對;集合的大小可以自動調節;通過泛型,我們可以約束可存放的類型,且取出時也不用強制類型轉換;集合只能保存引用類型的數據,基本類型的數據可以通過包裝類自動裝箱存入。
  2. List線性表是一種有序集合。某種意義上,它將數字索引與對象關聯起來。
    • List中,ArrayList在隨機訪問上效率很高;而LinkedList在插入刪除上效率很高。
  3. Map對象與其他對象關聯起來。
    • Map中,HashMap可以快速訪問元素;TreeMap將它的鍵以有序方式保存;LinkedHashMap 按照元素的插入順序保存,但也通過哈希實現快速訪問。
  4. Set中,相同的元素只能保存一個,Set的底層實現其實就是Map,所以它的子實現的特性與Map的子實現的特性很相似。
    • HashSet可以快速訪問元素;TreeSet 以有序方式保存元素;LinkedHashSet按照元素的插入順序保存。
  5. 集合類中的部分不常使用,且設計不合理的類被稱為 “遺留類”。
    • HashtableVectorStack,它們都是線程安全的,但不要在新代碼中使用它們。

關於集合還有很多可總結的,二次筆記時可以以集合為主題展開。
簡單集合分類

第 13 章 函數式編程#

函數式編程語言處理代碼片段就像處理數據一樣簡單。

就是有點費頭髮

函數式編程基於這樣一種想法:使用代碼以某種方式操縱其他代碼。我們並非從零開始構建代碼,而是從現有的、可靠的、經過測試的小片代碼開始組合在一起,創建新的代碼。

可以這樣理解函數式編程:面對對象編程抽象數據,函數式編程抽象行為。

由於函數式編程規定所有的數據必須是不可變的:設置一次,永不改變。這非常適合在並行編程場景中使用。

函數式編程

lambda表達式#

建議lambda表達式的行數控制還 3 行內,如果超過 3 行,考慮使用方法引用。

函數式介面#

當一個介面只包含一個抽象方法時,這種介面也叫 “功能介面”。

從抽象的層面,可以理解為這就是將方法作為參數或者返回值。但從JVM實現的角度,類和對象才是Java的一等公民,方法是依附於對象或類的,無法獨立存在,所以Java選擇將其與功能介面綁定在一起。

使用函數式介面時,名字並不重要,重要的只有參數類型和返回類型。當然對功能介面而言,其命名模式能幫助我們快速理解其作用,例如:

  1. 只處理對象的介面,命名通常為Function, ConsumerPredicate
  2. 只接受一個基本類型參數的介面,命名通常使用第一部分表示其基本類型,如LongConsumber, DoubleFunction, IntPredicate
  3. 返回類型為基本類型的介面,命名通常使用 'To' 來表示,如ToLongFunction<T>IntToLongFunction
  4. 參數類型與返回值類型相同的介面,命名使用OperatorUnaryOperator表示一個參數,BinaryOperator表示兩個參數;
  5. 接受一個參數並返回boolean類型的介面,命名使用Predicate
  6. 接受兩個參數的介面,命名通常使用BiXxx,如BiPredicate 表示接受兩個參數,返回類型為boolean

閉包#

內部類章節我們也講到閉包。閉包可以作為函數對象或者匿名函數,持有上下文數據,可以傳遞或保存。Java要求其變量是 == 最終 == 不可變的。

第 14 章 流#

集合優化了對象的存儲。而流(stream)與對象的成批處理有關。

我們平常說流,一般都是指I/O流,而用stream流指代本章所講的流。大多數時候,我們將對象存儲在一個集合中是為了處理它們,所以你會發現,自己編程的重點會從集合轉向流。流使我們的程序更小,可讀性更高,配合lambda表達式和方法引用,流大大提升了JDK 8的吸引力。

第 14 章 流

聲明式編程#

聲明式編程是一種有別於命令式編程的編程風格,我們聲明 “What to do”(做什麼),而不是像命令式編程那樣指明 “How to do”(怎麼做)。

內部迭代#

我們平常顯式使用for循環遍歷的迭代被稱為 “外部迭代”,而流使用 “內部迭代”:你不知道它具體是如何執行迭代的。然而,通過放寬對迭代方式的掌控,我們可以將其交給某種並行機制 —— 流的內部迭代效率更高。

惰性求值#

惰性求值意味著流只有在必要時才會被求值。你可以認為在 “被求值” 之前,流就像沒有被調用的方法一樣,只是聲明了邏輯;又或者流被聲明塑造成了能如此求值的模型,當真正需要 “被求值” 時,模型開始運行。

JDK 8對流的支持#

介面的默認方法也是在JDK 8新增的,它與流的添加或許有著千絲萬縷的聯繫。

在創造了流這個概念後,Java的設計者們遇到了個難題:如何將流整合進庫中,而又不影響已經使用了庫的代碼?因為,如果向介面加入新方法,會破壞每一個實現了該介面,但還沒有實現這個新方法的類。。。無所謂,默認方法會出手默認方法使得我們可以向介面添加默認實現,實現子類而不必重寫這些方法。

Optional類型#

設計Optional類型的初衷是為了避免流中發生異常而中斷。如果流中沒有元素,在執行某些流操作時會返回Optional對象。

reduce(BinaryOperator)#

形參命名(acc, i)#

由於reduce(BinaryOperator)傳入的lambda表達式中的第一個參數是上次調用時的結果,第二個參數是來自流中的新值,最好將第一個參數命名為acc、第二個參數命名為i用以見名之意。

第 15 章 異常#

Java的基本哲學是‘寫得不好的代碼無法運行’。

第 15 章 異常

缺陷:異常缺失#

異常使用不當可能會導致異常被 “吞掉” 的情況。

異常說明#

“異常說明” 可以縮小,但不可以擴大,這與類在繼承過程中的規則相反

Java的缺點#

除了內存的清理,其他清理都不會自動發生,必須告知客戶程序員,讓他們自己處理。

使用異常的準則#

  1. 儘可能使用try-with-resources
  2. 要在恰當的層次處理問題。除非你知道該如何處理,否則不要捕捉異常。
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。