第 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 章 內部類#
定義在另一個類中的類稱為內部類。
除了內部類的相關語法外,匿名內部類在後續會非常實用,應該熟練掌握。
內部類到外部類的鏈接#
內部類會隱含一個指向外部類對象的引用。因此內部類擁有訪問外部類所有元素的訪問權。也是因此,創建內部類時,為了讓內部類持有外部類對象的引用,必須先創建外部類對象,再通過外部類對象創建內部類。
匿名內部類#
public Contents contents() {
return new Contents() {
int i = 11;
};
}
這種聲明類的方式初看起來比較怪:我正寫到return
的時候,要返回一個Contents
對象了,哦~等等,讓我先定義一個Contents
的子類。但當類僅需要使用一次或極少重複使用時,也沒有必要專門為它寫一個類。
內部類的價值#
內部類的價值體現在:每個內部類可以獨立地繼承類或者實現介面,這完善了多重繼承問題的解決方案。
內部類還可以實現閉包。它能保留它被創建時所處作用域的信息。
第 12 章 集合#
如果對象的數量是固定的,而且這些對象的生命周期都是已知的,那麼這樣的程序是相當簡單的。
數組非常實用,但是其大小長度是已經固定了的,這會帶來很大的局限性,因為通常情況下我們並不能準確知道需要多少個對象。
Iterator
#
迭代器可以實現向任意類型的集合的正向遍歷。它讓程序員不用關心所處理的集合的類型(我們說不用關心,其實就是在說,我們不必考慮傳入集合的類型而被迫做不同的處理),它統一了對集合的訪問。
糟糕的設計 ——Stack
類#
儘管Java
提供了Stack
類,但這個類的設計非常糟糕(這很可能與其不合理的繼承關係有關),所以JDK 6
加入了ArrayDeque
,提供了直接使用棧的方法。
警鐘長鳴,謹慎考慮繼承關係!
面向介面#
通過面向介面而不是面向實現來編寫代碼,可以讓我們的代碼可以應用於更多對象類型。
總結#
Java
提供了很多持有對象的方式。最重要的四大類:List
、Map
、Set
,Queue
:
Collection
保存單個元素,Map
保存鍵值對;集合的大小可以自動調節;通過泛型,我們可以約束可存放的類型,且取出時也不用強制類型轉換;集合只能保存引用類型的數據,基本類型的數據可以通過包裝類自動裝箱存入。List
線性表是一種有序集合。某種意義上,它將數字索引與對象關聯起來。- 在
List
中,ArrayList
在隨機訪問上效率很高;而LinkedList
在插入刪除上效率很高。
- 在
Map
將對象與其他對象關聯起來。- 在
Map
中,HashMap
可以快速訪問元素;TreeMap
將它的鍵以有序方式保存;LinkedHashMap
按照元素的插入順序保存,但也通過哈希實現快速訪問。
- 在
Set
中,相同的元素只能保存一個,Set
的底層實現其實就是Map
,所以它的子實現的特性與Map
的子實現的特性很相似。HashSet
可以快速訪問元素;TreeSet
以有序方式保存元素;LinkedHashSet
按照元素的插入順序保存。
- 集合類中的部分不常使用,且設計不合理的類被稱為 “遺留類”。
Hashtable
、Vector
和Stack
,它們都是線程安全的,但不要在新代碼中使用它們。
關於集合還有很多可總結的,二次筆記時可以以集合為主題展開。
第 13 章 函數式編程#
函數式編程語言處理代碼片段就像處理數據一樣簡單。
就是有點費頭髮
函數式編程基於這樣一種想法:使用代碼以某種方式操縱其他代碼。我們並非從零開始構建代碼,而是從現有的、可靠的、經過測試的小片代碼開始組合在一起,創建新的代碼。
可以這樣理解函數式編程:面對對象編程抽象數據,函數式編程抽象行為。
由於函數式編程規定所有的數據必須是不可變的:設置一次,永不改變。這非常適合在並行編程場景中使用。
lambda
表達式#
建議lambda
表達式的行數控制還 3 行內,如果超過 3 行,考慮使用方法引用。
函數式介面#
當一個介面只包含一個抽象方法時,這種介面也叫 “功能介面”。
從抽象的層面,可以理解為這就是將方法作為參數或者返回值。但從JVM
實現的角度,類和對象才是Java
的一等公民,方法是依附於對象或類的,無法獨立存在,所以Java
選擇將其與功能介面綁定在一起。
使用函數式介面時,名字並不重要,重要的只有參數類型和返回類型。當然對功能介面而言,其命名模式能幫助我們快速理解其作用,例如:
- 只處理對象的介面,命名通常為
Function
,Consumer
和Predicate
; - 只接受一個基本類型參數的介面,命名通常使用第一部分表示其基本類型,如
LongConsumber
,DoubleFunction
,IntPredicate
; - 返回類型為基本類型的介面,命名通常使用 'To' 來表示,如
ToLongFunction<T>
和IntToLongFunction
; - 參數類型與返回值類型相同的介面,命名使用
Operator
。UnaryOperator
表示一個參數,BinaryOperator
表示兩個參數; - 接受一個參數並返回
boolean
類型的介面,命名使用Predicate
; - 接受兩個參數的介面,命名通常使用
BiXxx
,如BiPredicate
表示接受兩個參數,返回類型為boolean
。
閉包#
在內部類章節我們也講到閉包。閉包可以作為函數對象或者匿名函數,持有上下文數據,可以傳遞或保存。Java
要求其變量是 == 最終 == 不可變的。
第 14 章 流#
集合優化了對象的存儲。而流(stream)與對象的成批處理有關。
我們平常說流,一般都是指I/O
流,而用stream流
指代本章所講的流。大多數時候,我們將對象存儲在一個集合中是為了處理它們,所以你會發現,自己編程的重點會從集合轉向流。流使我們的程序更小,可讀性更高,配合lambda
表達式和方法引用,流大大提升了JDK 8
的吸引力。
聲明式編程#
聲明式編程是一種有別於命令式編程的編程風格,我們聲明 “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
的基本哲學是‘寫得不好的代碼無法運行’。
缺陷:異常缺失#
異常使用不當可能會導致異常被 “吞掉” 的情況。
異常說明#
“異常說明” 可以縮小,但不可以擴大,這與類在繼承過程中的規則相反
Java
的缺點#
除了內存的清理,其他清理都不會自動發生,必須告知客戶程序員,讓他們自己處理。
使用異常的準則#
- 儘可能使用
try-with-resources
。 - 要在恰當的層次處理問題。除非你知道該如何處理,否則不要捕捉異常。