JAVA IO 設計模式徹底分析
一。引子(概括地介紹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庫所有流處理器的起點。
JDK為程式設計師提供了大量的類庫,而為了保持類庫的可重用性,可擴充套件性和靈活性,其中使用到了大量的設計模式,本文將介紹JDK的I/O包中使用到的Decorator模式,並運用此模式,實現一個新的輸出流類。
介面卡類圖如下:
在看原始碼
Public class FileInputStream extends InputStream{
/* File Descriptor - handle to the open file */
private FileDescriptor fd;
public FileInputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);
}
fd = fdObj;
}
public FileInputStream(File file) throws FileNotFoundException {
String name = file.getPath();
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
fd = new FileDescriptor();
open(name);
}
//其它程式碼
}
我的理解:
它繼承了InputStrem型別,同時持有一個對FileDiscriptor的引用,是將一個FileDiscriptor物件適配成InputStrem型別的物件形式的介面卡模式。
Decorator模式簡介
Decorator模式又名包裝器(Wrapper),它的主要用途在於給一個物件動態的新增一些額外的職責。與生成子類相比,它更具有靈活性。
有時候,我們需要為一個物件而不是整個類新增一些新的功能,比如,給一個文字區新增一個滾動條的功能。我們可以使用繼承機制來實現這一功能,但 是這種方法不夠靈活,我們無法控制文字區加滾動條的方式和時機。而且當文字區需要新增更多的功能時,比如邊框等,需要建立新的類,而當需要組合使用這些功 能時無疑將會引起類的爆炸。
我們可以使用一種更為靈活的方法,就是把文字區嵌入到滾動條中。而這個滾動條的類就相當於對文字區的一個裝飾。這個裝飾(滾動條)必須與被裝飾 的元件(文字區)繼承自同一個介面,這樣,使用者就不必關心裝飾的實現,因為這對他們來說是透明的。裝飾會將使用者的請求轉發給相應的元件(即呼叫相關的方 法),並可能在轉發的前後做一些額外的動作(如新增滾動條)。通過這種方法,我們可以根據組合對文字區巢狀不同的裝飾,從而新增任意多的功能。這種動態的 對物件新增功能的方法不會引起類的爆炸,也具有了更多的靈活性。
以上的方法就是Decorator模式,它通過給物件新增裝飾來動態的新增新的功能。如下是Decorator模式的UML圖:
Component為元件和裝飾的公共父類,它定義了子類必須實現的方法。
ConcreteComponent是一個具體的元件類,可以通過給它新增裝飾來增加新的功能。
Decorator是所有裝飾的公共父類,它定義了所有裝飾必須實現的方法,同時,它還儲存了一個對於Component的引用,以便將使用者的請求轉發給Component,並可能在轉發請求前後執行一些附加的動作。
ConcreteDecoratorA和ConcreteDecoratorB是具體的裝飾,可以使用它們來裝飾具體的Component.
我的理解:
上圖中的抽象連結流是一個Decorator,它不真正的裝飾,它只提供介面,讓它的子類去裝飾。相當於上圖中的具體連結流去裝飾原始流。
JAVA IO包中的Decorator模式
JDK提供的java.io包中使用了Decorator模式來實現對各種輸入輸出流的封裝。以下將以java.io.OutputStream及其子類為例,討論一下Decorator模式在IO中的使用。
首先來看一段用來建立IO流的程式碼:
以下是程式碼片段:
try {
OutputStream out = new DataOutputStream(new FileOutputStream("test.txt"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
這段程式碼對於使用過JAVA輸入輸出流的人來說再熟悉不過了,我們使用DataOutputStream封裝了一個 FileOutputStream.這是一個典型的Decorator模式的使用,FileOutputStream相當於 Component,DataOutputStream就是一個Decorator.將程式碼改成如下,將會更容易理解:
以下是程式碼片段:
try {
OutputStream out = new FileOutputStream("test.txt");
out = new DataOutputStream(out);
} catch(FileNotFoundException e) {
e.printStatckTrace();
}
由於FileOutputStream和DataOutputStream有公共的父類OutputStream,因此對物件的裝飾對於使用者來說幾乎是透明的。下面就來看看OutputStream及其子類是如何構成Decorator模式的:
OutputStream是一個抽象類,它是所有輸出流的公共父類,其原始碼如下:
以下是程式碼片段:
public abstract class OutputStream implements Closeable, Flushable {
public abstract void write(int b) throws IOException;
……
}
它定義了write(int b)的抽象方法。這相當於Decorator模式中的Component類。
ByteArrayOutputStream,FileOutputStream 和 PipedOutputStream 三個類都直接從OutputStream繼承,以ByteArrayOutputStream為例:
以下是程式碼片段:
public class ByteArrayOutputStream extends OutputStream {
protected byte buf[];
protected int count;
public ByteArrayOutputStream() {
this(32);
}
public ByteArrayOutputStream(int size) {
if (size 〈 0) {
throw new IllegalArgumentException("Negative initial size: "
+ size);
}
buf = new byte[size];
}
public synchronized void write(int b) {
int newcount = count + 1;
if (newcount 〉 buf.length) {
byte newbuf[] = new byte[Math.max(buf.length 〈〈 1, newcount)];
System.arraycopy(buf, 0, newbuf, 0, count);
buf = newbuf;
}
buf[count] = (byte)b;
count = newcount;
}
……
}
它實現了OutputStream中的write(int b)方法,因此我們可以用來建立輸出流的物件,並完成特定格式的輸出。它相當於Decorator模式中的ConcreteComponent類。
接著來看一下FilterOutputStream,程式碼如下:
以下是程式碼片段:
public class FilterOutputStream extends OutputStream {
protected OutputStream out;
public FilterOutputStream(OutputStream out) {
this.out = out;
}
public void write(int b) throws IOException {
out.write(b);
}
……
}
同樣,它也是從OutputStream繼承。但是,它的建構函式很特別,需要傳遞一個OutputStream的引用給它,並且它將儲存對此 物件的引用。而如果沒有具體的OutputStream物件存在,我們將無法建立FilterOutputStream.由於out既可以是指向 FilterOutputStream型別的引用,也可以是指向ByteArrayOutputStream等具體輸出流類的引用,因此使用多層巢狀的方 式,我們可以為ByteArrayOutputStream新增多種裝飾。這個FilterOutputStream類相當於Decorator模式中的 Decorator類,它的write(int b)方法只是簡單的呼叫了傳入的流的write(int b)方法,而沒有做更多的處理,因此它本質上沒有對流進行裝飾,所以繼承它的子類必須覆蓋此方法,以達到裝飾的目的。
BufferedOutputStream 和 DataOutputStream是FilterOutputStream的兩個子類,它們相當於Decorator模式中的 ConcreteDecorator,並對傳入的輸出流做了不同的裝飾。以BufferedOutputStream類為例:
以下是程式碼片段:
public class BufferedOutputStream extends FilterOutputStream {
……
private void flushBuffer() throws IOException {
if (count 〉 0) {
out.write(buf, 0, count);
count = 0;
}
}
public synchronized void write(int b) throws IOException {
if (count 〉= buf.length) {
flushBuffer();
}
buf[count++] = (byte)b;
}
……
}
這個類提供了一個快取機制,等到快取的容量達到一定的位元組數時才寫入輸出流。首先它繼承了FilterOutputStream,並且覆蓋了父 類的write(int b)方法,在呼叫輸出流寫出資料前都會檢查快取是否已滿,如果未滿,則不寫。這樣就實現了對輸出流物件動態的新增新功能的目的。
下面,將使用Decorator模式,為IO寫一個新的輸出流。
自己寫一個新的輸出流
瞭解了OutputStream及其子類的結構原理後,我們可以寫一個新的輸出流,來新增新的功能。這部分中將給出一個新的輸出流的例子,它將 過濾待輸出語句中的空格符號。比如需要輸出"java io OutputStream",則過濾後的輸出為"javaioOutputStream".以下為SkipSpaceOutputStream類的程式碼:
以下是程式碼片段:
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* A new output stream, which will check the space character
* and won‘t write it to the output stream.
* @author Magic
*
*/
public class SkipSpaceOutputStream extends FilterOutputStream {
public SkipSpaceOutputStream(OutputStream out) {
super(out);
}
/**
* Rewrite the method in the parent class, and
* skip the space character.
*/
public void write(int b) throws IOException{
if(b!=‘ ’){
super.write(b);
}
}
}
它從FilterOutputStream繼承,並且重寫了它的write(int b)方法。在write(int b)方法中首先對輸入字元進行了檢查,如果不是空格,則輸出。
以下是一個 測試程式:
以下是程式碼片段:
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Test the SkipSpaceOutputStream.
* @author Magic
*
*/
public class Test {
public static void main(String[] args){
byte[] buffer = new byte[1024];
/**
* Create input stream from the standard input.
*/
InputStream in = new BufferedInputStream(new DataInputStream(System.in));
/**
* write to the standard output.
*/
OutputStream out = new SkipSpaceOutputStream(new DataOutputStream(System.out));
try {
System.out.println("Please input your words: ");
int n = in.read(buffer,0,buffer.length);
for(int i=0;i〈n;i++){
out.write(buffer[i]);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
執行以上測試程式,將要求使用者在console視窗中輸入資訊,程式將過濾掉資訊中的空格,並將最後的結果輸出到console視窗。比如:
以下是引用片段:
Please input your words:
a b c d e f
abcdef
總 結
在java.io包中,不僅OutputStream用到了Decorator設計模式,InputStream,Reader,Writer 等都用到了此模式。而作為一個靈活的,可擴充套件的類庫,JDK中使用了大量的設計模式,比如在Swing包中的MVC模式,RMI中的Proxy模式等等。 對於JDK中模式的研究不僅能加深對於模式的理解,而且還有利於更透徹的瞭解類庫的結構和組成。