天天看點

系統學習 Java IO (一)----輸入流和輸出流 InputStream/OutputStream

目錄:系統學習 Java IO ---- 目錄,概覽

InputStream

是Java IO API中所有輸入流的父類。

表示有序的位元組流,換句話說,可以将 InputStream 中的資料作為有序的位元組序列讀取。

這在從檔案讀取資料或通過網絡接收時非常有用。

InputStream 通常連接配接到某些資料源,如檔案,網絡連接配接,管道等

看如下代碼片段:

public class InputStreamExample {
    public static void main(String[] args) throws IOException {
        InputStream inputStream = new FileInputStream("D:\\out.txt");
        //do something with data...
        int data = inputStream.read();
        while (data != -1) {
            System.out.print((char) data);
            data = inputStream.read();
        }
        inputStream.close();
    }
}
           
注意:為了代碼清晰,這裡并沒有考慮處理異常的情況,IO 異常處理有專門的介紹。
read()

此方法傳回的是 int 值,其中包含讀取的位元組的位元組值,可以将傳回的 int 強制轉換為 char 輸出。

如果 read() 方法傳回 -1 ,則表示已到達流的末尾,這意味着在 InputStream 中不再有要讀取的資料。

也就是說,-1 作為 int 值,而不是 -1 作為 char 或 short,這裡有差別!

InputStream 類還包含兩個 read() 方法,這些方法可以将 InputStream 源中的資料讀入位元組數組。

這些方法是:

  • int read(byte[]);
  • int read(byte[], int offset, int length);

一次讀取一個位元組數比一次讀取一個位元組要快得多,是以在可以的時候,使用這些讀取方法而不是 read() 方法。

read(byte [])方法将嘗試将盡可能多的位元組讀入作為參數給出的位元組數組,因為數組具有空間。

該方法傳回一個 int ,其值是實際讀取了多少位元組,這點和 read() 方法不一樣。

如果可以從 InputStream 讀取的位元組少于位元組數組的空間,則位元組數組的其餘部分将包含與讀取開始之前相同的資料。例如:

InputStream input = new ByteArrayInputStream("123456789".getBytes());
byte[] bytes = new byte[4]; // 每次隻讀取 4 個位元組
int data = input.read(bytes);
while (data != -1) {
      System.out.print(new String(bytes));
      data = input.read(bytes);
}
           

将輸出 123456789678 ,而不是預期的 123456789 !

因為第一次讀取進 bytes 是 1234 ,第二次将是 5678 ,現在隻剩下 9 一個數字了,注意此時 bytes 的值是 5678 ,然後再讀取剩下 1個 9,不能裝滿 bytes 了,隻能覆寫 bytes的第一個位元組,最後傳回的bytes 是 9678。

是以記住檢查傳回的 int 以檢視實際讀入位元組數組的位元組數。

int read(byte[], int offset, int length);方法和 read(byte [])方法差不多,隻是增加了偏移量和指定長度。

和 read() 一樣,都是傳回 -1 表示資料讀取結束。

使用執行個體如下:

InputStream inputstream = new FileInputStream("D://out.txt");
byte[] data = new byte[1024];
int bytesRead = inputstream.read(data);
while(bytesRead != -1) {
  doSomethingWithData(data, bytesRead);
  bytesRead = inputstream.read(data);
}
inputstream.close();
           

首先,此示例建立一個位元組數組。

然後它建立一個名為 bytesRead 的 int 變量來儲存每次讀取 byte [] 調用時讀取的位元組數,

并立即配置設定 bytesRead 從第一次讀取 byte [] 調用傳回的值。

mark() and reset()

InputStream 類有兩個名為 mark() 和 reset() 的方法,InputStream 的子類可能支援也可能不支援:

  1. 該子類覆寫 markSupported() 并傳回true,則支援 mark( )和 reset() 方法。
  2. 該子類覆寫 markSupported() 并傳回 false ,則不支援 mark() 和 reset() 。
  3. 該子類不重寫 markSupported() 方法 ,則是父類的預設實作

    public boolean markSupported() { return false; }

    也是不支援 mark( )和 reset() 方法

mark() 在 InputStream 内部設定一個标記,預設值在位置 0 處。

可以手動标記到目前為止已讀取資料的流中的點,然後,代碼可以繼續從 InputStream 中讀取資料。

如果想要傳回到設定标記的流中的點,在 InputStream 上調用 reset() ,然後 InputStream “倒退”并傳回标記,

如此,便可再次從該mark點開始傳回(讀取)資料。很明顯這可能會導緻一些資料從 InputStream 傳回多次。我來舉個例子:

public static void testMarkAndReset() throws IOException {
        InputStream input = new ByteArrayInputStream("123456789".getBytes());
        System.out.println("第一次列印:");

        int count = 0;// 計算是第幾次讀取,将在第二次讀取時做标記;
        byte[] bytes = new byte[3]; // 每次隻讀取 3 個位元組
        int data = input.read(bytes);
        while (data != -1) {
            System.out.print(new String(bytes));
            if (++count == 2) { // 在第二輪讀取,即讀到數字 4 的時候,做标記
                input.mark(16); // 從 mark 點開始再過 readlimit 個位元組,mark 将失效
            }
            data = input.read(bytes);
        }

        input.reset();
        System.out.println("\n在經過 mark 和 reset 之後從 mark 位置開始列印:");
        data = input.read(bytes);
        while (data != -1) {
            System.out.print(new String(bytes));
            data = input.read(bytes);
        }
    }
           

将會輸出:

第一次列印:
123456789
在經過 mark 和 reset 之後從 mark 位置開始列印:
789  
           

另外要說明一下 mark(int readlimit) 參數,readlimit 是告訴系統,過了這個 mark 點之後,給本宮記住往後的 readlimit 個位元組,因為到時候 reset 之後,要從 mark 點開始讀取的;但實際情況和 jdk 文檔有出入,很多情況下調用 mark(int readlimit) 方法後,即使讀取超過 readlimit 位元組的資料,mark 标記仍有效,這又是為什麼呢?網上有人解答,但我還是決定親自探索一番。

我們這個執行個體引用的實際對象是

ByteArrayInputStream

先看一下它的源碼:

/* Note: The readAheadLimit for this class has no meaning.*/
    public void mark(int readAheadLimit) {
        mark = pos;
    }
           

好家夥,它說這個參數對于這個類沒有任何作用。

注意:這段是源碼分析可看可不看,跳過不影響閱讀

那我們在看看其他的 InputStream 子類,經驗證,FileInputStream 和一些實作類不支援 mark() 方法,我們看看

BufferedInputStream

類源碼:

我先把一些字段的含義說明一下:

  • count

    索引1大于緩沖區中最後一個有效位元組的索引。 該值始終在0到buf.length的範圍内; 元素buf [0]到buf [count-1]包含從底層輸入流獲得的緩沖輸入資料。在 read() 方法中讀完資料傳回 -1 就是因為

    if (pos >= count) return -1;

  • pos

    指緩沖區中的目前位置。 這是要從 buf 數組中讀取的下一個字元的索引。

    該值始終在 0 到 count 範圍内。 如果它小于 count,則 buf [pos] 是要作為輸入提供的下一個位元組; 如果它等于 count ,則下一個讀取或跳過操作将需要從包含的輸入流中讀取更多位元組。(即重新從輸入流中取出一段資料緩存)

  • markpos

    是調用最後一個 mark() 方法時 pos 字段的值。該值始終在-1到pos的範圍内。 如果輸入流中沒有标記位置,則此字段為-1。

BufferedInputStream 是每次讀取一定量的資料到 buf 數組中的,設定了 readlimit 肯定是想讓數組從 mark 索引開始至少記錄到 (mark + readlimit) 索引。

public synchronized void mark(int readlimit) {
        marklimit = readlimit;
        markpos = pos;
    }

public synchronized void reset() throws IOException {
        getBufIfOpen(); // Cause exception if closed
        if (markpos < 0)
            throw new IOException("Resetting to invalid mark");
        pos = markpos;
    }

 private void fill() throws IOException {
        byte[] buffer = getBufIfOpen();
        if (markpos < 0)
            pos = 0;            /* 如果标記點不在緩沖數組裡(沒标記點),丢掉buffer,取新資料 */
        else if (pos >= buffer.length)  /* 緩沖區中目前位置比buffer數組大,才執行下面代碼 */
            if (markpos > 0) {  /* 可以把 markpos 左邊的資料丢掉 */
                int sz = pos - markpos; // 需要緩存的位元組長度,從 markpos 開始
                System.arraycopy(buffer, markpos, buffer, 0, sz); // 複用記憶體空間
                pos = sz;
                markpos = 0;
            } else if (buffer.length >= marklimit) { // 如果 buffer 的長度已經大于 marklimit
                markpos = -1;   /* 那 mark 就失效了*/
                pos = 0;        /* 删除buffer内容,取新資料 */
            } else if (buffer.length >= MAX_BUFFER_SIZE) { // 如果buffer過長就抛錯
                throw new OutOfMemoryError("Required array size too large");
            } else {            /* buffer 還沒 marklimit 大,擴容到 pos 的2倍或者最大值 */
                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)) {
                    throw new IOException("Stream closed");
                }
                buffer = nbuf;
            }
        count = pos;
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
        if (n > 0)
            count = n + pos;
    }
           

可以得出:設定标記後,

  1. 如果緩沖區中目前位置比 buffer 數組小,也就是還沒讀完 buffer 數組,那 mark 标記不會失效;
  2. 下次繼續讀取,超過 buffer 大小位元組後,判斷 markpos 是否大于0,如果 markpos 大于0,即還在 buffer 數組内,則把 markpos 左邊的資料清除,markpos 指向 0 , 複用記憶體空間,并設定 buffer 的大小為 (pos - markpos) 的值;
  3. 再繼續讀取,此時 markpos 肯定不在 buffer 數組包含範圍了,此時判斷 buffer 的長度是否大于等于

    marklimit ,如果小于 marklimit ,那說明設定 mark 後讀取的資料長度還沒達到要求的 marklimit 了,給我繼續,保持從 mark 點開始緩存, mark 标記不會失效。然後 buffer 就擴容到

    Math.min(2倍 pos 或最大值 ,marklimit);

  4. 再繼續讀取,同上,buffer 這麼努力擴容,總有大于 marklimit 的時候,這時說明設定 mark 後繼續讀取的資料長度已經超過要求的 marklimit 了,仁盡義至,标記失效;

我們就隻分析了 ByteArrayInputStream 和 BufferedInputSteam 類的算法,其它輸入流不知道。是以 mark() 方法标記時,務必考慮好 readlimit 的值。

OutputStream

OutputStream 通常始終連接配接到某個資料目标,如檔案,網絡連接配接,管道等。 OutputStream 的目标是将資料寫入到外部。

write(byte)

write(byte) 方法用于将單個位元組寫入 OutputStream。 OutputStream 的 write() 方法接受一個 int ,其中包含要寫入的位元組的位元組值。 隻寫入 int 值的第一個位元組。 其餘的被忽略了。

OutputStream 的子類有重寫的 write() 方法。 例如,DataOutputStream 允許使用相應的方法writeBoolean(),writeDouble() 等編寫諸如 int,long,float,double,boolean 等 Java 基本類型。

write(byte[] bytes) , write(byte[] bytes, int offset, int length)

和 InputStream 一樣,它們也可以将一個數組或一部分位元組寫入 OutputStream 。

flush()

OutputStream 的flush() 方法将寫入 OutputStream 的所有資料重新整理到底層資料目标。 例如,如果 OutputStream 是 FileOutputStream ,則寫入 FileOutputStream 的位元組可能尚未完全寫入磁盤。 即使您的代碼已将其寫入 FileOutputStream ,但資料也可能還在某處緩存在記憶體中。 通過調用 flush() 可以確定将任何緩沖的資料重新整理(寫入)到磁盤(或網絡,或 OutputStream 的目标)。

如果覺得本文有所幫助,歡迎點【推薦】!文章錯誤之處煩請留言。

轉載說明:轉載後必須在文章開頭明顯地給出作者和原文連結;引用必須注明出處;需要二次修改釋出請聯系作者征得同意。

繼續閱讀