JAVA IO 設計模式徹底分析

來源:互聯網
上載者:User
 

 

一.引子(概括地介紹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庫所有流處理器的起點。

 今天晚上先到這了,明天再接著細看兩種設計模式具體是怎樣在I/O庫中被應用的。

 

 

JDK為程式員提供了大量的類庫,而為了保持類庫的可重用性,可擴充性和靈活性,其中使用到了大量的設計模式,本文將介紹JDK的I/O包中使用到的Decorator模式,並運用此模式,實現一個新的輸出資料流類。

Decorator模式簡介
     Decorator模式又名封裝器(Wrapper),它的主要用途在於給一個對象動態添加一些額外的職責。與產生子類相比,它更具有靈活性。
   有時候,我們需要為一個對象而不是整個類添加一些新的功能,比如,給一個文本區添加一個捲軸的功能。我們可以使用繼承機制來實現這一功能,但是這種方法不夠靈活,我們無法控制文本區加捲軸的方式和時機。而且當文本區需要添加更多的功能時,比如邊框等,需要建立新的類,而當需要組合使用這些功能時無疑將會引起類的爆炸。
   我們可以使用一種更為靈活的方法,就是把文本區嵌入到捲軸中。而這個捲軸的類就相當於對文本區的一個裝飾。這個裝飾(捲軸)必須與被裝飾的組件(文本區)繼承自同一個介面,這樣,使用者就不必關心裝飾的實現,因為這對他們來說是透明的。裝飾會將使用者的請求轉寄給相應的組件(即調用相關的方法),並可能在轉寄的前後做一些額外的動作(如添加捲軸)。通過這種方法,我們可以根據組合對文本區嵌套不同的裝飾,從而添加任意多的功能。這種動態對對象添加功能的方法不會引起類的爆炸,也具有了更多的靈活性。
   以上的方法就是Decorator模式,它通過給對象添加裝飾來動態添加新的功能。如下是Decorator模式的UML圖:

 

Component為組件和裝飾的公用父類,它定義了子類必須實現的方法。
ConcreteComponent是一個具體的組件類,可以通過給它添加裝飾來增加新的功能。
Decorator是所有裝飾的公用父類,它定義了所有裝飾必須實現的方法,同時,它還儲存了一個對於Component的引用,以便將使用者的請求轉寄給Component,並可能在轉寄請求前後執行一些附加的動作。
ConcreteDecoratorA和ConcreteDecoratorB是具體的裝飾,可以使用它們來裝飾具體的Component。

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中模式的研究不僅能加深對於模式的理解,而且還有利於更透徹的瞭解類庫的結構和組成。

 

 

 

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=551960

 

 

 

 大衛的Design Patterns學習筆記11:Decorator

一、        概述
繼承是對類進行擴充,以提供更多特性的一種基本方法,但是有時候,簡單的繼承可能不能滿足我們的需求。如我們的系統需要提供多種類型的產品:
類型A、類型B、...
同時,這些產品需要支援多種特性:
特性a、特性b、...
以下是兩種可能的實現:
1、繼承,分別實作類別型Aa、類型Ab、類型Ba、類型Bb、...
這種實現方式在類型的數目和所支援特性的數目眾多時會造成“類爆炸”,即會引入太多的類型,並且,這種實現的封裝性也很差,造成客戶代碼編寫十分困難,十分不可取。
2、修改各類型實現,在其中包含是否支援特性a、特性b、...選項,根據客戶選擇啟用各特性。這種實現是典型的MFC實現方法,但是這種實現只適合特性非常穩定的情況,否則,當特性發生增減時,各類型實現都可能需要修改。
因此,雖然類似的實現屢見不鮮,但以上兩種實現方式由於對特性的變化或者類型的變化過于敏感,無法滿足類型或特性動態變化的設計需求。如果我們可以將類型和特性分別定義,並且根據客戶代碼的需要動態對類型和特性進行組合,則可以克服上述問題。
正如Decorator(裝飾)模式的名字暗示的那樣,Decorator模式可以在我們需要為對象添加一些附加的功能/特性時發揮作用,除此之外,更為關鍵的是Decorator模式研究的是如何以對客戶透明的方式動態地給一個對象附加上更多的特性,換言之,用戶端並不會覺得對象在裝飾前和裝飾後有什麼不同。Decorator模式可以在不創造更多子類的情況下,將對象的功能加以擴充。Decorator模式使用原來被裝飾的類的一個子類的執行個體,把用戶端的調用委派到被裝飾類,Decorator模式的關鍵在於這種擴充是完全透明的。
正因為Decorator模式可以動態擴充decoratee所具有的特性,有人將其稱為“動態繼承模式”,該模式基於繼承,但與靜態繼承下進行功能擴充不同,這種擴充可以被動態賦予decoratee。

二、結構
Decorator模式的結構如所示:

圖1:Decorator模式類圖示意
在上面的類圖中包括以下組成部分:
1、Component(抽象構件)角色:給出一個抽象介面,以規範準備接收附加責任的對象。
2、Concrete Component(具體構件)角色:定義一個將要接收附加責任的類。
3、Decorator(裝飾)角色:持有一個Component對象的執行個體,並定義一個與抽象構件介面一致的介面。
4、Concrete Decorator(具體裝飾)角色:負責給構件對象“貼上”附加的責任。

三、應用
在以下情況下可以考慮使用Decorator模式:
1、在不影響其他對象的情況下,以動態、透明的方式給單個對象添加職責。(見樣本)
2、處理那些可以撤消的職責。(與上面類似,當有這種動態添加撤銷的需求時,可以為類添加相應的裝飾類成員,但想要撤銷裝飾時,將該成員設定為NULL即可,同樣,要支援動態切換也很容易)
3、當不能採用產生子類的方法進行擴充時。一種情況是,可能有大量獨立的擴充,為支援每一種組合將產生大量的子類,使得子類數目呈指數增長。另一種情況可能是因為類定義被隱藏,或類定義不能用於產生子類。(只要你學過排列組合,這一點應該不難理解)

Decorator和Adapter的不同在於前者不改變介面而後者則提供新的介面。可以將Decorator視為一個退化的、僅有一個組件的Composite,然而,Decorator的目的在於給對象添加一些額外的職責,而不是對象聚集。

既然Decorator模式如此強大,是不是可以大加推廣,大量運用Decorator來替代簡單的繼承呢?這樣,直接通過繼承來擴充類的功能就可以退出曆史舞台了!實際上這是不可能的,主要原因如下:
1、雖然“繼承破壞了封裝性”(父類向子類開放了過多的許可權),但是,繼承關係是客觀世界及OOP中最基本的關係,而且,繼承是深化介面規範的基礎,沒有繼承就沒有多態等諸多OO特性,因此,繼承比Decorator更常見,也更容易定義和實現;
2、由於Decorator動態疊加及不影響decoratee等特性的要求,Decorator很難用於複雜特性的定義;
3、Decorator是一種彙總與繼承的結合,應用Decorate模式還存在著其它一些限制,具體將在實現舉例部分討論。

四、優缺點
使用裝飾模式主要有以下的優點:
1、裝飾模式與繼承關係的目的都是要擴充項物件的功能,但是裝飾模式可以提供比繼承更多的靈活性。
2、通過使用不同的具體裝飾類以及這些裝飾類的排列組合,設計師可以創造出很多不同行為的組合。

使用裝飾模式主要有以下的缺點:
由於使用裝飾模式,可以比使用繼承關係需要較少數目的類,使用較少的類,固然使設計比較易於進行,但是,在另一方面,Decorator的缺點是會產生一些極為類似的小型對象,這些小型對象是為了提供極少量的特殊功能而定製的。

五、舉例
Decorator模式基本的實現方式如下:Decorator類從待修飾類ConcreteComponent的基類Component派生,以便與ConcreteComponent保持相同的介面,同時,在內部將所有函數調用轉寄給內部包容的ConcreteComponent對象來執行(1、被包容的ConcreteComponent對象通過Decorator的建構函式傳入。2、往往會附加一些“修飾”,否則,Decorator就徒有虛名了)。

在下面的例子中,xsstream用於對sstream進行Decorate,以統計調用operator < < 的次數,範例程式碼如下:

#include < iostream >
using namespace std;

struct stream
{
    virtual stream& operator < < (int i) = 0;
    virtual stream& operator < < (const char* str) = 0;
};

struct sstream : public stream
{
    stream& operator < < (int i)
    {
        cout < < i;
        return *this;
    }
    stream& operator < < (const char* str)
    {
        cout < < str;
        return *this;
    }
};

class xsstream : public stream
{
    static int count;
    stream* ps;
public:
    xsstream(stream* s) : ps(s) {}

    stream& operator < < (int i) {
        *ps < < "This is [" < < ++count < < "] call to operator < < . i = " < < i < < "
";
        return *this;
    }
    stream& operator < < (const char* str) {
        *ps < < "This is [" < < ++count < < "] call to operator < < . str = " < < str < < "
";
        return *this;
    }
};
int xsstream::count = 0;

int main() {
    sstream ss;
    stream* ps = new xsstream(&ss);
    stream& s = *ps;

    int i = 1;
    s < < i < < "abc";

    delete ps;

    return 0;
}

以上方法實現的Decorator模式實質上是對包容的擴充(由於只有一個ConcreteComponent,它看起來很像Proxy模式,但意圖不同),雖然以上樣本沒有什麼應用價值,但它基本闡明了Decorator實現的基本方法:
重新定義Decoratee中的介面方法(由於Decorator與Decoratee從相同基類派生,所以這是可能的),在其中添加必要的Decoration,並調用Decoratee的相應方法完成Decoration以外的工作,對於無需修飾的輔助方法,可以直接將方法調用轉寄給Decoratee。

但是,上述實現同時也告訴了我們Decorator模式存在的一個非常重要的限制,就是:
如果我們從抽象基類派生,我們必須實現抽象基類的每一個虛方法(或者,對於非虛基類,要麼重載基類的方法,要麼使用基類的實現,但這就丟失了ConcreteComponent子類中重載的實現,即失去(-)了特性,這與Decorator模式進行修飾,即加(+)特性的實質有悖),而當虛方法的數目眾多時,這將成為一種負擔。如實際basic_ostream實現的operator < < 有多種,分別用於bool、short、unsigned short、long、longlong...等等,逐一實現它們是一件很繁瑣的事情。
要解決這一問題,可以從sstream而不是stream派生,當我們只需要裝飾已經實現的一部分方法時這一招似乎“很管用”,但這有悖於Decorator模式的初衷。因為,Decorator模式提出的目的在於修飾一個類系,而不是單個的類。如果單純的為了修飾單個的類,簡單的繼承擴充即可解決問題,根本就無需從Decorator的角度來考慮這個問題,而如果面對的是多個類,這種方式使我們必須再次面對“類組合爆炸”的窘境。
幸好以上這個問題對於基於訊息/事件的應用中不存在,如在MFC中,進行介面修飾時,可以只需要特別修飾的訊息進行處理,而將所有其它訊息轉寄給待修飾的控制項。
這麼看來Decorator模式似乎在MFC應用中大有用武之地,但事實並非如此,之所以出現這種局面,大概是因為其一Decorator模式會使得客戶代碼變得複雜,我們必須自己建立特性,而不是在Create時直接指定,MFC的實現者希望MFC封裝類保持與API一樣的介面,在將特性在建立控制項時靜態指定與由使用者動態建立並指定之間,MFC的實現者選擇了前者;其二,從上面可以看出,Decorator模式對於小特性的定義比較合適,當特性或類系十分複雜時,Decorator模式很難做到面面俱到。

在Java中,java.io.LineNumberReader是一個典型的Decorator,可以Decorate整個Reader類系,用於在讀取檔案資訊的同時統計LineNumber資訊,你可以在readLine後通過LineNumberReader.getLineNumber方法擷取當前的行號,其實現比較簡單,有興趣的朋友可以研究一下。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.