從JDK原始碼看InputStream

超人汪小建發表於2017-11-07

概況

JDK 給我們提供了很多實用的輸入流 xxxInputStream,而 InputStream 是所有位元組輸入流的抽象。包括 ByteArrayInputStream 、FilterInputStream 、BufferedInputStream 、DataInputStream 和 PushbackInputStream 等等。

繼承結構

--java.lang.Object
  --java.io.InputStream複製程式碼

類定義

public abstract class InputStream implements Closeable複製程式碼

InputStream 被定為 public 且 abstract 的類,實現了Closeable介面。

Closeable 介面表示 InputStream 可以被close,介面定義如下:

public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}複製程式碼

主要屬性

private static final int MAX_SKIP_BUFFER_SIZE = 2048;

private static final int DEFAULT_BUFFER_SIZE = 8192;

private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;複製程式碼
  • MAX_SKIP_BUFFER_SIZE 表示輸入流每次最多能跳過的位元組數。
  • DEFAULT_BUFFER_SIZE 預設的緩衝大小。
  • MAX_BUFFER_SIZE 表示最大的緩衝陣列大小,這裡設定為 Integer.MAX_VALUE - 8 這裡也是考慮到 JVM 能支援的大小,超過這個值就會導致 OutOfMemoryError。

主要方法

read方法

一共有三個 read 方法,其中有一個抽象的 read 方法,其餘兩個 read 方法都會呼叫這個抽象方法,該方法用於從輸入流讀取下一個位元組,返回一個0到255範圍的值。如果已經到達輸入流結尾處而導致無可讀位元組則返回-1,同時,此方法為阻塞方法,解除阻塞的條件:

  1. 有可讀的位元組。
  2. 檢測到已經是輸入流的結尾了。
  3. 丟擲異常。

主要看第三個 read 方法即可,它傳入的三個引數,byte陣列、偏移量和陣列長度。該方法主要是從輸入流中讀取指定長度的位元組資料到位元組陣列中,需要注意的是這裡只是嘗試去讀取長度為 len 的陣列,但真正讀取到的陣列長度不一定為 len,返回值才是真正讀取到的長度。


    public abstract int read() throws IOException;

    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read();
        if (c == -1) {
            return -1;
        }
        b[off] = (byte)c;

        int i = 1;
        try {
            for (; i < len ; i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;
    }複製程式碼

看看它的邏輯,陣列為null則拋空指標,偏移量和長度超過邊界也拋異常,長度為0則什麼都不敢直接返回0。接著呼叫 read() 讀取一個位元組,如果為-1則說明結束,直接返回-1。否則繼續根據陣列長度迴圈呼叫 read() 方法讀取位元組,並且填充到傳入的陣列物件中,最後返回讀取的位元組數。

readAllBytes方法

該方法從輸入流讀取所有剩餘的位元組,在此過程是阻塞的,直到所有剩餘位元組都被讀取或到達流的結尾或發生異常。

邏輯是用一個 for 迴圈內嵌一個 while 迴圈,while 迴圈不斷呼叫 read 方法嘗試將 DEFAULT_BUFFER_SIZE 長度的位元組陣列填滿,一旦填滿則需要將陣列容量擴容一倍,再將原位元組陣列複製到新陣列中,然後再通過 while 迴圈繼續讀取,直到達到尾部才跳出 for 迴圈,最後返回讀取到的所有位元組陣列。

    public byte[] readAllBytes() throws IOException {
        byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
        int capacity = buf.length;
        int nread = 0;
        int n;
        for (;;) {
            while ((n = read(buf, nread, capacity - nread)) > 0)
                nread += n;
            if (n < 0)
                break;
            if (capacity <= MAX_BUFFER_SIZE - capacity) {
                capacity = capacity << 1;
            } else {
                if (capacity == MAX_BUFFER_SIZE)
                    throw new OutOfMemoryError("Required array size too large");
                capacity = MAX_BUFFER_SIZE;
            }
            buf = Arrays.copyOf(buf, capacity);
        }
        return (capacity == nread) ? buf : Arrays.copyOf(buf, nread);
    }複製程式碼

readNBytes方法

從輸入流中讀取指定長度的位元組,而且它能保證一定能讀取到指定的長度,它屬於阻塞方式,用一個 while 迴圈不斷呼叫 read 讀取位元組,直到讀取到指定長度才結束讀取。

    public int readNBytes(byte[] b, int off, int len) throws IOException {
        Objects.requireNonNull(b);
        if (off < 0 || len < 0 || len > b.length - off)
            throw new IndexOutOfBoundsException();
        int n = 0;
        while (n < len) {
            int count = read(b, off + n, len - n);
            if (count < 0)
                break;
            n += count;
        }
        return n;
    }複製程式碼

available方法

返回從該輸入流能進行非阻塞讀取的剩餘位元組數,當呼叫 read 讀取的位元組數一般會小於該值,有一些InputStream的子實現類會通過該方法返回流的剩餘總位元組數,但有些並不會,所以使用時要注意點。

這裡抽象類直接返回0,子類中重寫該方法。

public int available() throws IOException {
        return 0;
    }複製程式碼

skip方法

從輸入流中跳過指定個數字節,返回值為真正跳過的個數。這裡的實現是簡單通過不斷呼叫 read 方法來實現跳過邏輯,但這是較低效的,子類可用更高效的方式重寫此方法。

下面看看邏輯,最大的跳過長度不能超過 MAX_SKIP_BUFFER_SIZE ,並且用一個 while 迴圈呼叫 read 方法,如果遇到返回為-1,即已經到達結尾了,則跳出迴圈。可以看到 skipBuffer 其實是沒有什麼作用,直接讓其被 GC 即可,最後返回真正跳過的位元組數。

    public long skip(long n) throws IOException {

        long remaining = n;
        int nr;

        if (n <= 0) {
            return 0;
        }

        int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
        byte[] skipBuffer = new byte[size];
        while (remaining > 0) {
            nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
            if (nr < 0) {
                break;
            }
            remaining -= nr;
        }

        return n - remaining;
    }複製程式碼

close方法

此方法用於關閉輸入流,並且釋放相關資源 。

public void close() throws IOException {}複製程式碼

transferTo方法

從輸入流中按順序讀取全部位元組並且寫入到指定的輸出流中,返回值為轉移的位元組數。轉移過程中可能會發生不確定次的阻塞,阻塞可能發生在 read 操作或 write 操作。

主要邏輯是用 while 迴圈不斷呼叫 read 方法操作讀取位元組,然後呼叫輸出流的 write 方法寫入,直到讀取返回-1,即達到結尾。最後返回轉移的位元組數。

    public long transferTo(OutputStream out) throws IOException {
        Objects.requireNonNull(out, "out");
        long transferred = 0;
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int read;
        while ((read = this.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
            out.write(buffer, 0, read);
            transferred += read;
        }
        return transferred;
    }複製程式碼

markSupported方法

是否支援 mark 和 reset 操作,這裡直接返回 false,子類根據實際重寫該方法。

    public boolean markSupported() {
        return false;
    }複製程式碼

mark方法

標記輸入流當前位置,與之對應的是 reset 方法,通過他們之間的組合能實現重複讀取操作。另外它會傳入 readlimit 引數,它用於表示該輸入流中在執行 mark 操作後最多可以讀 readlimit 個位元組後才使 mark 的位置失效。

可以看到 InputStream 的 mark 方法是什麼都不做的,子類中再具體實現。

public synchronized void mark(int readlimit) {}複製程式碼

reset方法

與 mark 方法對應,它可以重置輸入流的位置到上次被 mark 操作標識的位置。InputStream 的 reset 方法直接丟擲一個 IOException,子類中根據實際情況實現。

    public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }複製程式碼

以下是廣告相關閱讀

========廣告時間========

鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以到 item.jd.com/12185360.ht… 進行預定。感謝各位朋友。

為什麼寫《Tomcat核心設計剖析》

=========================

相關閱讀:

從JDK原始碼角度看Object
從JDK原始碼角度看Long
從JDK原始碼角度看Integer
volatile足以保證資料同步嗎
談談Java基礎資料型別
從JDK原始碼角度看併發鎖的優化
從JDK原始碼角度看執行緒的阻塞和喚醒
從JDK原始碼角度看併發競爭的超時
從JDK原始碼角度看java併發執行緒的中斷
從JDK原始碼角度看Java併發的公平性
從JDK原始碼角度看java併發的原子性如何保證
從JDK原始碼看Writer
從JDK原始碼看關閉鉤子
從JDK原始碼角度看Byte
從JDK原始碼角度看Boolean
從JDK原始碼角度看Short

歡迎關注:

這裡寫圖片描述
這裡寫圖片描述

這裡寫圖片描述
這裡寫圖片描述

相關文章