Java 的 IO 系統采用了裝飾器設計模式。其 IO 分為面向字節和面向字符兩種,面向字節以字節為輸入輸出單位,面向字符以字符為輸入輸出單位。此外,在每部分中,又分為輸入和輸出兩部分,相互對應,如InputStream
類型和OutputStream
類型。再往下分,又分為數據源類型和裝飾器類型。數據源類型表示的是數據的來源和去處,而裝飾器類型可以給輸入輸出賦予額外的功能。
在使用中,為了得到我們需要的輸入輸出功能,我們常常需要將一個數據源對象和多個裝飾器對象組合起來。例如,我們需要從本地文件中以緩沖的方式按字節讀入數據的話,就需要將一個FileInputStream
對象和一個BufferedInputStream
對象組合起來,其中 FileInputStream
對象負責從文件中按字節為單位讀取數據,而 BufferedInputStream
對象負責對讀取數據進行緩沖。
如果不明白裝飾模式的話,Java IO 會變的難以理解。而如果不清楚 Java IO 的結構的話,又會覺得它難以使用。這篇博客結合裝飾模式介紹了 Java IO 的結構,以及部分 IO 類的實現。這其實是我的學習筆記,如有不足,歡迎指出。
一、輸入源
我們以輸入為例,講解 Java IO 的結構。輸入的基本功能是將數據從某個輸入源中讀取出來。這個輸入源可能是文件,也有可能是一個 ByteArray
對象,也有可能是一個 String
對象。數據源不同,讀入的方式也不同。因此,Java 的開發者為每種輸入源編寫了相應的輸入類,有從文件中讀入數據的 FileInputStream
,有從 ByteArray
對象中讀入數據的 ByteArrayInputStream
,……。為了統一接口,減少重復代碼的編寫,Java 的設計者從這些輸入類中,抽取出了相同的部分,編寫了抽象輸入類 InputStream
,作為所有輸入類的基類。到目前為止,類圖可以整理如下,為了方便敘述,省略了一些方法和成員變量。
其中,InputStream
是一個抽象類,它是所有輸入源的父類。它規定了輸入源的接口,其中,read()
為從輸入源中讀入一個字節,并以返回值的形式返回。而 read(byte[] b)
為從輸入源中讀入一塊數據到 byte[] b
中,其返回值為實際讀入的字節數。而 read(byte[] b, int off, int len)
則為從輸入源讀入 len
個字節,填充到 byte[] b
的 b[off]
及之后的位置上。
由于輸入源的讀入操作因輸入源而異,因此,InputStream
中的 read()
方法是抽象的,由具體的輸入源子類實現。
在 InputStream
中,read(byte[] b)
和 read(byte[] b, int off, int len)
都是調用 read()
來實現的,即不斷地使用 read()
來一個個地讀入字節,并放到 byte[] b
的合適位置上。但這樣讀取,效率其實并不高。以搬磚為例,我們從 A 處搬 10 塊磚給 B 處砌墻的老師傅。以 InputStream
的邏輯來搬運的話,我們需要從 A 處拿起一塊磚,跑到 B 處,把磚給老師傅,跑回 B 處,再拿起一塊……。多跑了好多趟,浪費了好多時間,力氣大的話,完全可以拿起 10 塊磚,一次性搬完。所以,在其大多數子類中,都重寫了這些方法。
由于讀取文件需要調用操作系統的系統調用,需要用 C/C++
來完成,所以,在 FileInputStream
中,有兩個 native
方法,read0()
和 readBytes(byte[] b, int off, int len)
,分別用來調用系統調用讀取文件中的 1 個字節和調用系統調用讀取文件中的 1 堆字節。其他的讀取方法都是通過調用這兩個方法來實現的。
二、裝飾器
有了輸入源之后,我們已經可以完成各種讀入數據的操作了。我們可以從數據源中讀取一個字節,或者一堆字節。但是,出于性能以及其他方面的考慮,我們通常還會給輸入操作添加一些功能,如緩沖。
1. 緩沖
之前講過一個搬磚的例子,我們要從 A 處搬 10 塊磚給 B 處的老師傅,考慮到老師傅今天砌墻任務繁重,之后很可能會再讓我們去給他搬磚,于是我們不如一次性多給他搬幾塊過去放在 B 處,他再要磚我們直接從 B 處拿給他就好了,就不用再跑去 A 處搬磚過來了。這樣就節省了許多傳輸的時間。
緩沖就是這么個道理。我們通常會給輸入和輸出都設立一個緩沖區。考慮到之后很可能會再次讀取數據,在讀入數據時,除了我們需要的數據之外,還會多讀一些數據進來,放到緩沖區里。每次讀入數據之前,都會先看看緩沖區里有沒有我們要的數據,如果有的話就從緩沖區中讀入,沒有的話再去數據源里讀取。而在輸出數據時,會先把數據輸出到緩沖區里去,當緩沖區滿了,再將緩沖區里的數據全部輸出到目的地里。
注意:緩沖區的讀寫還要考慮數據的一致性問題,這里沒有過多的闡述。
2. 裝飾器類
就像緩沖一樣,我們通常會給輸入輸出加上一些額外的功能。于是問題來了,我們怎么才能讓每種輸入源都具備這些功能呢?最簡單的,就是為每一種輸入源的每種額外功能都寫一個類,就像下面這樣(為了讓圖小一點,省略了其他的輸入源)。
這樣的設計會帶來許多問題。
- 首先,類太多了。在不考慮功能組合的情況下,如果有 m 個輸入源,要實現 n 個功能,那就需要寫 m 乘 n 個類,考慮功能組合的話,還要更多。
- 其次,重復代碼太多。其實同一個功能的代碼都差不多,但要給每個輸入源都寫一遍。寫的時候麻煩,到時候要改這個功能的代碼,還得一個個改過去,不利于維護。
為了解決上面的問題,Java 的設計人員將各個功能拎了出來,給每個功能單獨寫了功能類,如通過 BufferedInputStream
類來為輸入源提供緩沖功能,通過 DataInputStream
類來為輸入源提供基本類型數據的讀入功能。請注意,此時,功能類僅僅提供了功能,它本身并不能從輸入源中讀取數據,所以在功能類內部都會有一個數據源類的成員變量,從數據源中讀取數據的操作都是通過這個成員變量來完成的。就像下面這樣:
class Func1Decorator extends InputStream {
private InputStream in;
Func1Decorator(InputStream in){
this.in = in;
}
public int read() {
...
a = in.read();
...
}
...
}
知識點:其實從這里可以看出,組合比繼承要更靈活,因為組合可以和多態結合。
在功能類初始化時,就從外界傳入了輸入源對象,其后,從數據源讀取數據的操作都由這個對象負責,而功能類僅負責對讀入的數據進行處理來完成其功能。
注意到,這里的功能類還繼承了輸入源類 InputStream
。一方面,這是因為從外界看來,功能類確實是一個 InputStream
,它實現了 InputStream
中所有的接口。它的語意是一個帶有 Func1
功能的 InputStream
。另一方面,這也方便了功能的組合,當功能類同時也是 InputStream
時,要組合兩個功能到一起時,只需要按一定的順序把一個功能類的對象看作輸入源對象傳入進去即可。如:
DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream("filename")));
上面這段代碼創建了一個能讀取基本數據類型數據并帶有緩沖的文件輸入對象。因為功能類也是一個 InputStream
,它可以被當作其他功能類的數據源類,其他的功能類會在它的 read
方法的基礎上,繼續拓展自己的功能。
其實,之前我們所說的功能類就是裝飾器,用來給基礎類擴展功能。而這種用組合語法利用多態為基礎類擴展功能的模式就是裝飾模式。
3. 裝飾器模式的優點
裝飾模式分離了裝飾類和被裝飾類的邏輯。裝飾器類中保持了一個被裝飾對象的引用,當裝飾器類需要底層的功能時,只需要通過這個引用調用對應方法即可,并不需要了解其具體邏輯。這對代碼的維護有很大的幫助。
裝飾模式可以減少類的數量。在前面我們已經看到了,用純繼承語法來擴展功能需要為每種基礎類和功能的各種組合編寫類,類的數量會非常地多。而通過裝飾器模式,我們只需要寫幾個裝飾器類就可以了。裝飾器類中保持的被裝飾對象的引用,會發揮其多態性,我們傳入什么基礎類對象,就執行對應的方法。這使得一個裝飾器類可以和幾乎所有基礎類(及其子類,從語義上來說,子類是特殊的父類)結合產生相應的擴展類。
裝飾模式的擴展性很好。當要為基礎類擴展新的功能時,用純繼承語法需要為每種基礎類,為另外的各種功能組合編寫類。但使用裝飾器模式的話,只需要編寫一個裝飾器類即可。
裝飾模式利用了組合語法,在復用代碼時,組合語法與繼承語法相比有一個明顯的優點,就是可以利用多態,從而根據組合對象的不同能夠產生不同的語義。
三、結構
裝飾模式的通用類圖如下:
在我們之前的敘述中,是沒有中間這個 Decorator
抽象類的。它是所有裝飾器類的父類,它一方面可以使類的結構更加清晰,另一方面這個抽象類可以減少各個子類中重復邏輯的書寫。當然,我們剛才所敘述的也是裝飾模式,只不過沒有了 Decorator
抽象類,所有的裝飾器類都是直接繼承自 Component
的。這是一種簡化的裝飾模式。當裝飾器數量比較少時,可以省略裝飾器基類。另外在確定只有一種 Component
時,可以不寫 Component
基類,用那一個 ConcreteComponent
來代替 Component
基類。
下面是 Java IO 的類圖,只畫了字節流的輸入部分,其他部分相似。另外,因為頁面的大小是有限的,而且一些類在類結構中的位置是相似的,所以省略了一些類。
其中,FilterInputStream
就是裝飾模式中的 Decorator
基類。繼承自它的都是裝飾器類,它們為輸入擴展了功能。
四、參考資料
- JDK文檔
- 《Thinking in Java》
- 《設計模式之禪》