從Decorator,Adapter模式看Java/IO庫

講到Decorator模式的時候,不能不提到它的實際應用--在Java/IO庫裏面的應用,<<Java與模式>>這本書也不例外,有點不一樣的是,這本書在介紹的時候有個專題,是從兩個模式來看Java/IO庫,完這個專題後,個人感覺對Java/IO庫有了全新的認識同時也加深了Decorator模式跟Adapter適配器模式的理解,現和大家分享下這個在我看來很偉大的成果,同時說明下,以下大部分文字跟圖片是來自<<Java與模式>>這本書。

 

 一.引子(概括地介紹Java的IO)

 無論是哪種編程語言,輸入跟輸出都是重要的一部分,Java也不例外,而且Java將輸入/輸出的功能和使用範疇做了很大的擴充。它採用了流的機制來實現輸入/輸出,所謂流,就是數據的有序排列,而流可以是從某個源(稱爲流源或Source of Stream)出來,到某個目的地(稱爲流匯或Sink of Stream)去的。由流的方向,可以分成輸入流和輸出流,一個程序從輸入流讀取數據向輸出流寫數據。

 如,一個程序可以用FileInputStream類從一個磁盤文件讀取數據,如下圖所示:


 像FileInputStream這樣的處理器叫做流處理器,它就像流的管道一樣,從一個流源吸入某種類型的數據,並輸出某種類型的數據。上面這種示意圖叫做流的管道圖。

 同樣道理,也可以用FileOutputStream類向一個磁盤文件寫數據,如下圖所示:


 

 在實際應用這種機制並不沒有太大的用處,程序需要寫出地通常是非常結構化的信息,因此這些byte類型的數據實際上是一些數值,文字,源代碼等。Java的I/O庫提供了一個稱做鏈接(Chaining)的機制,可以將一個流處理器跟另一個流處理器首尾相接,以其中之一的輸出爲輸入,形成一個流管道的鏈接。

 例如,DataInputStream流處理器可以把FileInputStream流對象的輸出當作輸入,將Byte類型的數據轉換成Java的原始類型和String類型的數據。如下圖所示:


 

 類似地,向一個文件寫入Byte類型的數據不是一個簡單的過程。一個程序需要向一個文件裏寫入的數據往往都是結構化的,而Byte類型則是原始類型。因此在寫的時候必須經過轉換。DataOutputStream流處理器提供了接收了原始數據類型和String數據類型,而這個流處理器的輸出數據則是Byte類型。也就是說DataOutputStream可以將源數據轉換成Byte類型的數據,再輸出來。

 這樣一來,就可以將DataOutputStream與FileOutputStream鏈接起來,這樣程序就可以將原始數據類型和String類型的源數據寫入這個鏈接好的雙重管道里面,達到將結構化數據寫到磁盤文件裏面的目的,如下圖所示:


 

 這又是鏈接的所發揮的大作用。

 流處理器所處理的流必定都有流源,而如果將流類所處理的流源分類的話,基本可以分成兩大類:

 第一 數組,String,File等,這一種叫原始流源。

 第二 同樣類型的流用做鏈接流類的流源,叫鏈接流源。

 

二 Java I/O庫的設計原則

 Java語言的I/O庫是對各種常見的流源,流匯以及處理過程的抽象化。客戶端的Java程序不必知道最終的流源,流匯是磁盤上的文件還是數組等;也不必關心數據是否經過緩衝的,可否按照行號讀取等處理的細節。

 書中提到了,對於第一次見到Java/IO庫的人,無不因爲這個庫的龐雜而感到困惑;而對於熟悉這個庫的人,而又常常爲這個庫的設計是否得當而爭論不體。書的作者提出自己的意見,要理解Java I/O這個龐大而複雜的庫,關鍵是要掌握兩個對稱性跟兩個設計模式模式。

 

Java I/O庫具有兩個對稱性,它們分別是:

 1 輸入-輸出對稱性,比如InputStream和OutputStream各自佔據Byte流的輸入與輸出的兩個平行的等級結構的根部。而Reader和Writer各自佔據Char流的輸入與輸出的兩個平行的等級結構的根部。

 2 byte-char對稱,InputStream和Reader的子類分別負責Byte和Char流的輸入;OutputStream和Writer的子類分別負責Byte和Char流的輸出,它們分別形成平行的等級結構。

 

Java I/O庫的兩個設計模式:

Java的I/O庫總體設計是符合裝飾者模式(Decorator)跟適配器模式(Adapter)的。如前所述,這個庫中處理流的類叫做流類。引子裏所談到的FileInputStream,FileOutputStream,DataInputStream及DataOutputStream都是流處理器的例子。

 

1 裝飾者模式:在由InputStream,OutputStream,Reader和Writer代表的等級結構內部,有一些流處理器可以對另一些流處理器起到裝飾作用,形成新的,具有改善了的功能的流處理器。裝飾者模式是Java I/O庫的整體設計模式。這樣的一個原則是符合裝飾者模式的,如下圖所示:


2 適配器模式:在由InputStream,OutputStream,Reader和Writer代表的等級結構內部,有一些流處理器是對其它類型的流源的適配。這就是適配器模式的應用,如下圖所示。

  

 適配器模式應用到了原始流處理器的設計上面,構成了I/O庫所有流處理器的起點。

 

 

三 裝飾模式的應用

 學過裝飾模式後,大家會發現,它在Java語言中最著名的應用莫過於Java I/O標準爲庫的設計了。這一節將以處理Byte流爲例,看看裝飾模式是怎樣得到應用的。

 

 爲什麼不用繼承而用裝飾模式

 我們知道Java I/O庫需要很多性能的各種組合,如果說這些性能的組合是通過繼承方式來實現的話,那麼每一種組合都需要一個類,這樣就會出現大量重複性問題的出現,從而使類數目“爆炸”。而如果採用裝飾模式,那麼不僅類的數目大減少了,性能的重複也可以減至到最少。所以裝飾模式是Java I/O庫的基本模式。在這裏我想再用<<Head First Design Pattern>>中講到裝飾模式時候的一個例子,看看裝飾模式是怎麼達到不僅類的數目大減少了,性能的重複也可以減至到最少。

 再回到Java I/O庫,由於裝飾模式的引用,造成了靈活性和複雜都大大增加了,我們在使用Java I/O庫時,必須理解Java I/O庫是由一些基本的原始流處理器和圍繞它們的裝飾流處理器所組成的,這樣可以在學習和使用Java I/O庫時達到事半功倍的效果。

 下面我用<<Java與模式>>,<<Head First Design Pattern>>或者是網上看到的一些類圖來分析:

 

 首先是InputStream類型中的裝飾模式:

 InputStream有七個直接的具體子類,有四個屬於FilterInputStream的具體子類,如下圖所示:

  上圖中所有的類都叫做流處理器,這個圖就叫做(InputStream類型的)流處理器圖。

  書中提到根據輸入流的源的類型,可以將這些流類分成兩種,即原始流類(Original Stream)和鏈接流處理器(Wrapper Stream)。

 

  原始流處理器

  原始流處理器接收一個Byte數組對象,String對象,FileDiscriptor對象或者不同類型的流源對象,根據上面的圖,原始流處理器包括以下四種:

  ByteArrayInputStream:爲多線程的通信提供緩衝區操作功能,接收一個Byte數組作爲流的源。

  FileInputStream:建立一個與文件有關的輸入流。接收一個File對象作爲流的源。

  PipedInputStream:可以與PipedOutputStream配合使用,用於讀入一個數據管道的數據,接收一個PipedOutputStream作爲源。

  StringBufferInputStream:將一個字符串緩衝區轉換爲一個輸入流。接收一個String對象作爲流的源。(JDK幫助文檔上說明:已過時。此類未能正確地將字符轉換爲字節。從JDK1.1開始,從字符串創建流的首選方法是通過StringReader類進行創建。只有字符串中每個字符的低八位可以由此類使用。)

 

鏈接流處理器

  所謂鏈接流處理器,就是可以接收另一個流對象作爲源,並對之進行功能擴展的類。InputStream類型的鏈接處理器包括以下幾種,它們都接收另一個InputStream對象作爲流源。

  (1)FilterInputStream稱爲過濾輸入流,它將另一個輸入流作爲流源。這個類的子類包括以下幾種:

  BufferedInputStream:用來從硬盤將數據讀入到一個內存緩衝區中,並從緩衝區提供數據。

  DataInputStream:提供基於多字節的讀取方法,可以讀取原始類型的數據。

  LineNumberInputStream:提供帶有行計數功能的過濾輸入流。

  PushbackInputStream:提供特殊的功能,可以將已經讀取的字節“推回”到輸入流中。

  (2)ObjectInputStream可以將使用ObjectInputStream串行化的原始數據類型和對象重新並行化。

  (3)SeqcueneInputStream可以將兩個已有的輸入流連接起來,形成一個輸入流,從而將多個輸入流排列構成一個輸入流序列。

  抽象結構圖

  按照上面的這種原始流處理器和鏈接流處理器的劃分,可以用下面的結構圖來描述它們之間的關係。

  

 

 上面的流處理器圖跟裝飾模式的結構圖有着顯而易見的相同之處。實際上InputStream類型的流處理器結構確實符合裝飾模式。 

 裝飾模式結構圖

 

  對於上圖FilterInputStream查看JDK1.6源代碼,部分代碼如下:

Java代碼  收藏代碼
  1. public class FilterInputStream extends InputStream {  
  2.     protected volatile InputStream in;   
  3.   
  4.     protected FilterInputStream(InputStream in) {  
  5.        this.in = in;  
  6.     }  
  7.     //其它代碼  
  8. }  

 

FilterInputStream繼承了InputStream,也引用了InputStream,而它有四個子類,這就是所謂的Decorator模式。
  上面這個圖向我們傳達了這個信息:鏈接流對象接收一個原始流對象或者另外一個鏈接流對象作爲流源;另一方面他們對流源的內部工作方法做了相應的改變,這種改變是裝飾模式所要達到的目的。比如:

  BufferedInputStream“裝飾”了InputStream的內部工作方式,使得流的讀入操作使用了緩衝機制。在使用了緩衝機制後,不會對每一次的流讀入操作都產生一個物理的讀盤動作,從而提高了程序的效率,在汲及到物理流的讀入時,都應當使用這個裝飾流類。

  LineNumberInputStream和PushbackInputStream也同樣“裝飾”了InputStream的內部工作方式,前者使得程序能夠按照行號讀入數據;後者能夠使程序讀入的過程中,退後一個字符。

  DataInputStream子類讀入各種不同的原始數據類型以及String類型的數據,這一點可以從它提供的各種read方法看出來,如:readByte(),readInt(),readFloat()等。

  Java語言的I/O庫提供了四大等級結構:InputStream,OutputStream,Reader,Writer四個系列的類。InputStream和OutputStream處理8位字節流數據, Reader和Writer處理16位的字符流數據。InputStream和Reader處理輸入, OutputStream和Writer處理輸出,所以OutputStream,Reader,Writer這三類的裝飾模式跟前面詳細介紹的InputStream裝飾模式大同小異,大家可以看書中其它部分對這三類的詳細描述或者從網上也能找到有關資料。爲了方便比較這幾種類型,順便附上Java語言的I/O層次結構圖:

 下面的圖表示:以InputStream和OutputStream形成的層次關係



 下面的圖表示:以Reader和Writer形成的層次關係



 

 

四 適配器模式的應用

 適配器模式是Java I/O庫中第二個最爲重要的設計模式。

InputStream原始流處理器中的適配器模式,InputStream類型的原始流處理器是適配器模式的應用。

ByteArrayInputStream是一個適配器類
 ByteArrayInputStream繼承了InputStream的接口,而封裝了一個byte數組。換言之,它將一個byte數組的接口適配成InputStream流處理器的接口。
 我們知道Java語言支持四種類型:Java接口,Java類,Java數組,原始類型(即int,float等)。前三種是引用類型,類和數組的實例是對象,原始類型的值不是對象。
也即,Java語言的數組是像所有的其他對象一樣的對象,而不管數組中所存儲的元素類型是什麼。
這樣一來的話,ByteArrayInputStream就符合適配器模式的描述,是一個對象形式的適配器類。

 

FileInputStream是一個適配器類
在FileInputStream繼承了InputStrem類型,同時持有一個對FileDiscriptor的引用。這是將一個FileDiscriptor對象適配成InputStrem類型的對象形式的適配器模式。

查看JDK1.6的源代碼我們可以看到:

 

Java代碼  收藏代碼
  1. public  class FileInputStream extends InputStream  
  2. {  
  3.     private FileDescriptor fd;  
  4.   
  5.     private FileChannel channel = null;  
  6.   
  7.  public FileInputStream(File file) throws FileNotFoundException {  
  8.       String name = (file != null ? file.getPath() : null);  
  9.       SecurityManager security = System.getSecurityManager();  
  10.       if (security != null) {  
  11.         security.checkRead(name);  
  12.        }  
  13.         if (name == null) {  
  14.             throw new NullPointerException();  
  15.         }  
  16.        fd = new FileDescriptor();  
  17.        open(name);  
  18.     }  
  19.   
  20.    //其它代碼  
  21. }  

 

StringBufferInputStream繼承了InputStream類型,同時持有一個對String對象的引用,這是一個將String對象適配成InputStream類型的對象形式的適配器模式。

OutputStream原始流處理器中的適配器模式,同樣地,在OutputStream類型中,所有的原始流處理器都是適配器類。

ByteArrayOutputStream繼承了OutputStream類型,同時持有一個對byte數組的引用。它一個byte數組的接口適配成OutputString類型的接口,因此也是一個對象形式的適配器模式的應用。

 

FileOutputStream是一個適配器類
FileOutputStream繼承了OutputStream類型,同時持有一個對FileDiscriptor對象的引用。這是一個將FileDiscriptor接口適配成OutputStream接口形式的對象形適配器模式。

Reader原始流處理器中的適配器模式,Reader類型的原始流處理器都是適配器模式的應用。

 

StringReader是一個適配器類
StringReader類繼承了Reader類型,持有一個對String對象的引用。它將String的接口適 配成Reader類型的接口。

 

從byte流到char流的適配
在Java I/O庫中,使用比較頻繁的要數InputStreamReader,OutputStreamWriter這兩種類了,
InputStreamReader是從byte輸入流到char輸入流的一個適配器。

當把InputStreamReader與任何InputStream的具體子類鏈接的時候,可以從InputStream的輸出讀入byte類型的數據,將之轉換成爲char類型的數據,如下圖所示:


查看JDK1.6的InputStreamReader源代碼:

 

Java代碼  收藏代碼
  1. public class InputStreamReader extends Reader {  
  2.   
  3.     private final StreamDecoder sd;  
  4.   
  5.     public InputStreamReader(InputStream in) {  
  6.        super(in);  
  7.         try {  
  8.            sd = StreamDecoder.forInputStreamReader(in, this, (String)null);   
  9.           // ## check lock object  
  10.         } catch (UnsupportedEncodingException e) {  
  11.      // The default encoding should always be available  
  12.      throw new Error(e);  
  13.  }  
  14.   
  15.  //其它代碼  
  16. }  

 

其中StreamDecoder是sun.nio.cs這個包裏的一個類OutputStreamWriter是適配器類。

Java代碼  收藏代碼
  1.  35   public class StreamDecoder extends Reader  
  2.    36   {  
  3.    58       public static StreamDecoder forInputStreamReader(InputStream in,  
  4.    59                                                        Object lock,  
  5.    60                                                        String charsetName)  
  6.    61           throws UnsupportedEncodingException  
  7.    62       {  
  8.    63           String csn = charsetName;  
  9.    64           if (csn == null)  
  10.    65               csn = Charset.defaultCharset().name();  
  11.    66           try {  
  12.    67               if (Charset.isSupported(csn))  
  13.    68                   return new StreamDecoder(in, lock, Charset.forName(csn));  
  14.    69           } catch (IllegalCharsetNameException x) { }  
  15.    70           throw new UnsupportedEncodingException (csn);  
  16.    71       }  
  17.   
  18.             // Exactly one of these is non-null  
  19.   225       private InputStream in;  
  20.   226       private ReadableByteChannel ch;  
  21.   227     
  22.   228       StreamDecoder(InputStream in, Object lock, Charset cs) {  
  23.   229           this(in, lock,  
  24.   230            cs.newDecoder()  
  25.   231            .onMalformedInput(CodingErrorAction.REPLACE)  
  26.   232            .onUnmappableCharacter(CodingErrorAction.REPLACE));  
  27.   233       }  
  28.   234     
  29.   235       StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {  
  30.   236           super(lock);  
  31.   237           this.cs = dec.charset();  
  32.   238           this.decoder = dec;  
  33.   239     
  34.   240           // This path disabled until direct buffers are faster  
  35.   241           if (false && in instanceof FileInputStream) {  
  36.   242           ch = getChannel((FileInputStream)in);  
  37.   243           if (ch != null)  
  38.   244               bb = ByteBuffer.allocateDirect(DEFAULT_BYTE_BUFFER_SIZE);  
  39.   245           }  
  40.   246           if (ch == null) {  
  41.   247           this.in = in;  
  42.   248           this.ch = null;  
  43.   249           bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);  
  44.   250           }  
  45.   251           bb.flip();                      // So that bb is initially empty  
  46.   252       }  
  47.               //其它代碼  
  48.   
  49. }  

 

同樣道理我們能得出OutputStringWriter是從OutputStream到Writer的適配器類。也就是說,與任何一個OutputStream的具體子類相鏈接時,OutputStringWriter可以將OutputStream類型的byte流適配成爲char流。它的源代碼跟上面的InputStreamReader差不多,這就不貼出來,感興趣可以查看JDK1.4在線源碼這本書後面還有個小例子,附有一些講解,我就不列出來了,有書的可以看看。

 

五 總結

在這三篇文章裏主要是汲及到三個知識點:知識點一: Java I/O庫的四大等級結構
Java語言的I/O庫提供了四大等級結構:InputStream,OutputStream,Reader,Writer四個系列的類。InputStream和OutputStream處理8位字節流數據, Reader和Writer處理16位的字符流數據。InputStream和Reader處理輸入, OutputStream和Writer處理輸出。
知識點二: Decorator模式在Java I/O庫的應用
知識點三: Adapter模式在Java I/O庫的應用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章