天天看點

java io -- FilterInputStream 與 裝飾者模式

在上一篇文章中講了FileInputStream

本文将介紹一個特殊的輸入位元組流:FilterInputStream,以及與之相随的一個經典的設計模式,裝飾者模式。

在之前的文章中提到,InputStream的讀取是以byte為機關的,但是我們日常中經常會讀寫其他類型的資料,當然,我們可以把讀取出來的位元組進行轉碼,轉成我們需要的資料,那麼能不能直接讀取字元,int等資料呢?能,隻需要利用DataInputStream這個類。

 1 FilterInputStream 剖析

FilterInputStream是InputStream一個特殊的子類,關于它和InputStream的關系,可以參照這篇文章java io -- InputStream,它有一個很重要filed:

protected volatile InputStream in;
           

而這個類的特殊之處,就是包含了一個InputStream,使得可以在這個InputStream基礎上進行多種封裝,進而達到裝飾的目的。

上一張這個類的結構圖如下:

java io -- FilterInputStream 與 裝飾者模式

結構圖有些說不太清楚,我這裡把簡略後的源碼展示出來:

package java.io;

public class FilterInputStream extends InputStream {
    /**
     * The input stream to be filtered.
     */
    protected volatile InputStream in;

    /**
     * Creates a <code>FilterInputStream</code>
     * by assigning the  argument <code>in</code>
     * to the field <code>this.in</code> so as
     * to remember it for later use.
     *
     * @param   in   the underlying input stream, or <code>null</code> if
     *          this instance is to be created without an underlying stream.
     */
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

    /**
     * Reads the next byte of data from this input stream. The value
     * byte is returned as an <code>int</code> in the range
     * <code>0</code> to <code>255</code>. If no byte is available
     * because the end of the stream has been reached, the value
     * <code>-1</code> is returned. This method blocks until input data
     * is available, the end of the stream is detected, or an exception
     * is thrown.
     * <p>
     * This method
     * simply performs <code>in.read()</code> and returns the result.
     *
     * @return     the next byte of data, or <code>-1</code> if the end of the
     *             stream is reached.
     * @exception IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    public int read() throws IOException {
        return in.read();
    }

    /**
     * Reads up to <code>byte.length</code> bytes of data from this
     * input stream into an array of bytes. This method blocks until some
     * input is available.
     * <p>
     * This method simply performs the call
     * <code>read(b, 0, b.length)</code> and returns
     * the  result. It is important that it does
     * <i>not</i> do <code>in.read(b)</code> instead;
     * certain subclasses of  <code>FilterInputStream</code>
     * depend on the implementation strategy actually
     * used.
     *
     * @param      b   the buffer into which the data is read.
     * @return     the total number of bytes read into the buffer, or
     *             <code>-1</code> if there is no more data because the end of
     *             the stream has been reached.
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#read(byte[], int, int)
     */
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    /**
     * Reads up to <code>len</code> bytes of data from this input stream
     * into an array of bytes. If <code>len</code> is not zero, the method
     * blocks until some input is available; otherwise, no
     * bytes are read and <code>0</code> is returned.
     * <p>
     * This method simply performs <code>in.read(b, off, len)</code>
     * and returns the result.
     *
     * @param      b     the buffer into which the data is read.
     * @param      off   the start offset in the destination array <code>b</code>
     * @param      len   the maximum number of bytes read.
     * @return     the total number of bytes read into the buffer, or
     *             <code>-1</code> if there is no more data because the end of
     *             the stream has been reached.
     * @exception  NullPointerException If <code>b</code> is <code>null</code>.
     * @exception  IndexOutOfBoundsException If <code>off</code> is negative,
     * <code>len</code> is negative, or <code>len</code> is greater than
     * <code>b.length - off</code>
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    public int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }
    

    /**
     * Closes this input stream and releases any system resources
     * associated with the stream.
     * This
     * method simply performs <code>in.close()</code>.
     *
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    public void close() throws IOException {
        in.close();
    }

}
           

從源碼中可以看出,這個FilterInputStream中有一個域:

protected volatile InputStream in;
           

這個域是在構造方法中傳入的:

protected FilterInputStream(InputStream in) {
    this.in = in;
}
           

這也就說明了FilterInputStream在執行個體化的時候,要傳一個InputStream類的對象進來。

而且這個類中的read方法并不像FileInputStream進行了實作,而隻是一種“僞”實作:

public int read() throws IOException {
    return in.read();
}
           

其實隻是用了這個構造方法傳入的這個InputStream的read方法。

上面隻是介紹了一下這個類,那麼在java中,這個FilterInputStream有什麼作用呢?這就要從它的幾個子類說起了,我再上一張FilterInputStream的類圖,先看看它的幾個子類。

java io -- FilterInputStream 與 裝飾者模式

FilterInpustStream子類可以分成兩類:

1) DataInputStream能以一種與機器無關的方式,直接從地從位元組輸入流讀取JAVA基本類型和String類型的資料。

2 )其它的子類使得能夠對InputStream進行改進,即在原有的InputStream基礎上可以提供了新的功能特性。日常中用的最多的就是ButtferInputStream,使得inputStream具有緩沖的功能。

接下來我們就以DataInputStream和BufferInputStream對此進行深入剖析。

DataInputStream

之前的InputStream我們隻能讀取byte,這個類使得我們可以直接從stream中讀取int,String等類型。先把這個類幾個方法及說明列出來如下(部分):

Method Summary

Methods 
Modifier and Type Method and Description
int

read(byte[] b)

Reads some number of bytes from the contained input stream and stores them into the buffer array b.

int

read(byte[] b, int off, int len)

Reads up to len bytes of data from the contained input stream into an array of bytes.

boolean

readBoolean()

See the general contract of the readBoolean method of DataInput.

byte

readByte()

See the general contract of the readByte method of DataInput.

char

readChar()

See the general contract of the readChar method of DataInput.

void

readFully(byte[] b, int off, int len)

See the general contract of the readFully method of DataInput.

int

readInt()

See the general contract of the readInt method of DataInput.

String

readUTF()

See the general contract of the readUTF method of DataInput.

static String

readUTF(DataInput in)

Reads from the stream in a representation of a Unicode character string encoded in modified UTF-8 format; this string of characters is then returned as a String.

有了DataInputStream後,我們就可以直接讀取int,boolean了,下面有一個例子,簡單說明DataInputStream的使用。

try {
        DataOutputStream out = new DataOutputStream(new FileOutputStream("/home/zhaohui/tmp/readPrim"));
        out.writeInt(123);
        out.writeUTF("你好");
        out.writeBoolean(true);
        out.flush();
        out.close();

        DataInputStream in = new DataInputStream(new FileInputStream("/home/zhaohui/tmp/readPrim"));
        int a = in.readInt();
        System.out.println("first int is "+a);
        String b = in.readUTF();
        System.out.println("second string is "+b);
        boolean c= in.readBoolean();
        System.out.println("third boolean is "+c);
        in.close();

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
           

輸出:

first int is 123

second string is 你好

third boolean is true

在例子中,我們将一個FileInputStream傳入到DataInputStream中,進而使得我們可以直接對檔案讀取寫入int,或者boolean等。

DataInputStream原理

所有的輸入流都是對byte的操作,這個類能直接讀取int,說明内部一定是對byte進行了處理,我們看一下readInt()方法的源碼。

public final int readInt() throws IOException {
    int ch1 = in.read();
    int ch2 = in.read();
    int ch3 = in.read();
    int ch4 = in.read();
    if ((ch1 | ch2 | ch3 | ch4) < 0)
        throw new EOFException();
    return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
}
           

正如我在前一篇文章中所說,java每個int是4個位元組,而InputStream的read是面向位元組的,也就是每次隻能讀取1個位元組,是以在readInt這個方法中,讀取出4個位元組,再進行處理成一個int,這裡我們不對處理過程進行深究。

對于其他的方法思想大緻一樣,不過由于對于String類型需要對字元進行編碼,對字元的長度進行傳遞,會複雜一點,這裡就不多說了,關鍵是這個類算是對InputStream的一個封裝。

BufferedInputStream

我這裡再介紹另一個常用的FilterInputStream類,BufferedInputStream類。

這個類提供了一個緩存來加速我們從輸入流的讀取。

由于我們從InputStream中讀取資料時,一般都會用到os的io,或者網絡io,這都是會耗費大量時間的操作,比如我們現在從檔案讀取前20個位元組,過一會又從檔案讀取20個位元組,這就是兩次io,好的,有了BufferedInputStream,就解決這個兩次io的問題,這個類在read時,幹脆多讀一部分資料進來,放在記憶體裡,等你每次操作流的時候,讀取的資料直接從記憶體中就可以拿到,這就減少了io次數,加快我們的io。

我這裡隻解析BufferedInputStream中的read()方法,有興趣的可以在jdk裡檢視其他部分。

public class BufferedInputStream extends FilterInputStream {
//....省略部分源碼
private static int DEFAULT_BUFFER_SIZE = 8192;

private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

protected volatile byte buf[];

public synchronized int read() throws IOException {
    if (pos >= count) {
        fill();
        if (pos >= count)
            return -1;
    }
    return getBufIfOpen()[pos++] & 0xff;
}
private void fill() throws IOException {
    byte[] buffer = getBufIfOpen();
    if (markpos < 0)
        pos = 0;            /* no mark: throw away the buffer */
    else if (pos >= buffer.length)  /* no room left in buffer */
        if (markpos > 0) {  /* can throw away early part of the buffer */
            int sz = pos - markpos;
            System.arraycopy(buffer, markpos, buffer, 0, sz);
            pos = sz;
            markpos = 0;
        } else if (buffer.length >= marklimit) {
            markpos = -1;   /* buffer got too big, invalidate mark */
            pos = 0;        /* drop buffer contents */
        } else if (buffer.length >= MAX_BUFFER_SIZE) {
            throw new OutOfMemoryError("Required array size too large");
        } else {            /* grow buffer */
            int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
                    pos * 2 : MAX_BUFFER_SIZE;
            if (nsz > marklimit)
                nsz = marklimit;
            byte nbuf[] = new byte[nsz];
            System.arraycopy(buffer, 0, nbuf, 0, pos);
            if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
                // Can't replace buf if there was an async close.
                // Note: This would need to be changed if fill()
                // is ever made accessible to multiple threads.
                // But for now, the only way CAS can fail is via close.
                // assert buf == null;
                throw new IOException("Stream closed");
            }
            buffer = nbuf;
        }
    count = pos;
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    if (n > 0)
        count = n + pos;
}

}
           

從上面的代碼中看到,這個類有一個buf[],也就是用來當做緩存的位元組數組。每次調用read讀取資料時,先檢視要讀取的資料是否在緩存中,如果在緩存中,直接從緩存中讀取;如果不在緩存中,則調用fill方法,從InputStream中讀取一定的存儲到buf中。

是以利用BufferedInputStream讀取資料時,在一定的情況下是可以加速的。

裝飾者模式

在講了FilterInputStream之後,就必須要提到一個設計模式:裝飾者模式(decorator pattern)。

我這裡不是專門講解裝飾者模式的,隻是說明裝飾者模式在流裡的應用。

裝飾者模式,顧名思義,是對原有類進行了一定的裝飾,裝飾後的類必須和原有的類擁有相同的方法,當然,可以在原有類的基礎上進行擴充。

這裡的裝飾者模式通過包含一個原有的Inputstream對象,并且将InputStream原有的方法或直接暴露,或進行裝飾後暴露,又或者添加了新的特性,如DataInputStream中的readInt(),BufferedInputStream中的緩存功能。

其實這裡還有一個話題,為什麼InputStream選擇裝飾者模式,而非直接繼承的方法來擴充,這就是裝飾者模式VS繼承。

為了回答這個問題,如果我用了繼承,看看我們的類圖是什麼樣的。

java io -- FilterInputStream 與 裝飾者模式

如圖所示,我展示的還隻是一部分類圖,如果單純的使用繼承,就會造成類的“爆炸”式增長。

我在這裡做個簡單的分析,為什麼造成這種爆炸式的增長。

在InputStream的直接子類中,如FileInputStream,都是定義了輸入流的<來源>,或者說是媒體,通過檔案,或者網絡。而FilterInputStream的子類并不是增加了流的來源,而隻是改善了流讀取方法,比如添加了緩存,直接讀取int,String等類型。

可以這樣簡單的認為,InputStream的直接子類是“目的”,而FilterInpustStream的子類是“方法”,我們用一個InputStream就是要用目的和方法。

直接使用繼承,可以實作“目的”和“方法”,但是每一種來源的輸入流,都需要改善流讀取方法,是以在使用繼承時,每一個InputStream的子類都需要DataInputStream,BufferedInputStream這幾個類提供的“裝飾作用”的功能,是以需要的類的數目就是A*B的數目。

而直接使用裝飾者模式,将InputStream的幾個直接子類進一步抽象,在此基礎上提供裝飾作用,所需要的類的數目是A+B。使用裝飾者模式使得java類的更有層次性,類的數目得到充分控制。這就是裝飾者模式相比于繼承的優勢。