天天看點

Java IO 裝飾模式簡析

Java 的 IO 系統采用了裝飾器設計模式。其 IO 分為面向位元組和面向字元兩種,面向位元組以位元組為輸入輸出機關,面向字元以字元為輸入輸出機關。此外,在每部分中,又分為輸入和輸出兩部分,互相對應,如

InputStream

類型和

OutputStream

類型。再往下分,又分為資料源類型和裝飾器類型。資料源類型表示的是資料的來源和去處,而裝飾器類型可以給輸入輸出賦予額外的功能。

Java IO 裝飾模式簡析

Java IO的結構

在使用中,為了得到我們需要的輸入輸出功能,我們常常需要将一個資料源對象和多個裝飾器對象組合起來。例如,我們需要從本地檔案中以緩沖的方式按位元組讀入資料的話,就需要将一個

FileInputStream

對象和一個

BufferedInputStream

對象組合起來,其中 

FileInputStream

 對象負責從檔案中按位元組為機關讀取資料,而 

BufferedInputStream

 對象負責對讀取資料進行緩沖。

如果不明白裝飾模式的話,Java IO 會變的難以了解。而如果不清楚 Java IO 的結構的話,又會覺得它難以使用。這篇部落格結合裝飾模式介紹了 Java IO 的結構,以及部分 IO 類的實作。這其實是我的學習筆記,如有不足,歡迎指出。

一、輸入源

我們以輸入為例,講解 Java IO 的結構。輸入的基本功能是将資料從某個輸入源中讀取出來。這個輸入源可能是檔案,也有可能是一個 

ByteArray

 對象,也有可能是一個 

String

 對象。資料源不同,讀入的方式也不同。是以,Java 的開發者為每種輸入源編寫了相應的輸入類,有從檔案中讀入資料的 

FileInputStream

,有從 

ByteArray

 對象中讀入資料的 

ByteArrayInputStream

,……。為了統一接口,減少重複代碼的編寫,Java 的設計者從這些輸入類中,抽取出了相同的部分,編寫了抽象輸入類 

InputStream

,作為所有輸入類的基類。到目前為止,類圖可以整理如下,為了友善叙述,省略了一些方法和成員變量。

Java IO 裝飾模式簡析

輸入源的結構

其中,

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. 裝飾器類

就像緩沖一樣,我們通常會給輸入輸出加上一些額外的功能。于是問題來了,我們怎麼才能讓每種輸入源都具備這些功能呢?最簡單的,就是為每一種輸入源的每種額外功能都寫一個類,就像下面這樣(為了讓圖小一點,省略了其他的輸入源)。

Java IO 裝飾模式簡析

不使用裝飾器模式時的類結構

這樣的設計會帶來許多問題。

  • 首先,類太多了。在不考慮功能組合的情況下,如果有 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. 裝飾模式的擴充性很好。當要為基礎類擴充新的功能時,用純繼承文法需要為每種基礎類,為另外的各種功能組合編寫類。但使用裝飾器模式的話,隻需要編寫一個裝飾器類即可。

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

三、結構

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

Java IO 裝飾模式簡析

裝飾器模式的通用類圖

在我們之前的叙述中,是沒有中間這個 

Decorator

 抽象類的。它是所有裝飾器類的父類,它一方面可以使類的結構更加清晰,另一方面這個抽象類可以減少各個子類中重複邏輯的書寫。當然,我們剛才所叙述的也是裝飾模式,隻不過沒有了 

Decorator

 抽象類,所有的裝飾器類都是直接繼承自 

Component

的。這是一種簡化的裝飾模式。當裝飾器數量比較少時,可以省略裝飾器基類。另外在确定隻有一種 

Component

 時,可以不寫 

Component

 基類,用那一個 

ConcreteComponent

 來代替 

Component

 基類。

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

Java IO 裝飾模式簡析

Java IO 的結構

其中,

FilterInputStream

 就是裝飾模式中的 

Decorator

 基類。繼承自它的都是裝飾器類,它們為輸入擴充了功能。

四、參考資料

  • JDK文檔
  • 《Thinking in Java》
  • 《設計模式之禅》

每天都在分享文章,也每天都有人想要我出來給大家分享下怎麼去學習Java。大家都知道,我們是學Java全棧的,大家就肯定以為我有全套的Java系統教程。沒錯,我是有Java全套系統教程,進扣裙【47】974【9726】所示,今天小編就免費送!~

後記:對于大部分轉行的人來說,找機會把自己的基礎知識補齊,邊工作邊補基礎知識,真心很重要。 

“我們相信人人都可以成為一個程式員,現在開始,找個師兄,帶你入門,學習的路上不再迷茫。這裡是ja+va修真院,初學者轉行到網際網路行業的聚集地。"