關于 Java IO(一):裝飾模式

Java 的 IO 系統采用了裝飾器設計模式。其 IO 分為面向字節和面向字符兩種,面向字節以字節為輸入輸出單位,面向字符以字符為輸入輸出單位。此外,在每部分中,又分為輸入和輸出兩部分,相互對應,如InputStream類型和OutputStream類型。再往下分,又分為數據源類型和裝飾器類型。數據源類型表示的是數據的來源和去處,而裝飾器類型可以給輸入輸出賦予額外的功能。

Java IO的結構

在使用中,為了得到我們需要的輸入輸出功能,我們常常需要將一個數據源對象和多個裝飾器對象組合起來。例如,我們需要從本地文件中以緩沖的方式按字節讀入數據的話,就需要將一個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[] bb[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. 裝飾器模式的優點

  1. 裝飾模式分離了裝飾類和被裝飾類的邏輯。裝飾器類中保持了一個被裝飾對象的引用,當裝飾器類需要底層的功能時,只需要通過這個引用調用對應方法即可,并不需要了解其具體邏輯。這對代碼的維護有很大的幫助。

  2. 裝飾模式可以減少類的數量。在前面我們已經看到了,用純繼承語法來擴展功能需要為每種基礎類和功能的各種組合編寫類,類的數量會非常地多。而通過裝飾器模式,我們只需要寫幾個裝飾器類就可以了。裝飾器類中保持的被裝飾對象的引用,會發揮其多態性,我們傳入什么基礎類對象,就執行對應的方法。這使得一個裝飾器類可以和幾乎所有基礎類(及其子類,從語義上來說,子類是特殊的父類)結合產生相應的擴展類。

  3. 裝飾模式的擴展性很好。當要為基礎類擴展新的功能時,用純繼承語法需要為每種基礎類,為另外的各種功能組合編寫類。但使用裝飾器模式的話,只需要編寫一個裝飾器類即可。

裝飾模式利用了組合語法,在復用代碼時,組合語法與繼承語法相比有一個明顯的優點,就是可以利用多態,從而根據組合對象的不同能夠產生不同的語義

三、結構

裝飾模式的通用類圖如下:

裝飾器模式的通用類圖

在我們之前的敘述中,是沒有中間這個 Decorator 抽象類的。它是所有裝飾器類的父類,它一方面可以使類的結構更加清晰,另一方面這個抽象類可以減少各個子類中重復邏輯的書寫。當然,我們剛才所敘述的也是裝飾模式,只不過沒有了 Decorator 抽象類,所有的裝飾器類都是直接繼承自 Component 的。這是一種簡化的裝飾模式。當裝飾器數量比較少時,可以省略裝飾器基類。另外在確定只有一種 Component 時,可以不寫 Component 基類,用那一個 ConcreteComponent 來代替 Component 基類。

下面是 Java IO 的類圖,只畫了字節流的輸入部分,其他部分相似。另外,因為頁面的大小是有限的,而且一些類在類結構中的位置是相似的,所以省略了一些類。

Java IO 的結構

其中,FilterInputStream 就是裝飾模式中的 Decorator 基類。繼承自它的都是裝飾器類,它們為輸入擴展了功能。

四、參考資料

  • JDK文檔
  • 《Thinking in Java》
  • 《設計模式之禪》
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容

  • 概述 java.io 包幾乎包含了所有操作輸入、輸出需要的類。所有這些流類代表了輸入源和輸出目標。java.io ...
    Steven1997閱讀 9,216評論 1 25
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,775評論 18 139
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,313評論 11 349
  • 早晨醒來 滿屏都是雪的消息 這讓我記起 小時候 媽媽喊 下雪了,好大的雪 快起來看雪
    第一閑人閱讀 132評論 0 6
  • 美好的時光總是短暫的,埃里克森教練的科學與藝術模塊二中文課程,在2017年最后一天結束了。 學員們和老師及課程組織...
    徐立文Levin閱讀 884評論 0 2