OpenJDK 源碼閱讀之 Java 字節流輸入類的實現

Java 的輸入輸出總是給人一種很混亂的感覺,要想把這個問題搞清楚,必須對各種與輸入輸出相關的類之間的關係有所瞭解。只有你瞭解了他們之間的關係,知道設計這個類的目的是什麼,才能更從容的使用他們。

我們先對 Java I/O 的總體結構進行一個總結,再通過分析源代碼,給出把每個類的關鍵功能是如何實現的。

Java I/O 的主要結構

Java 的輸入輸出,主要分爲以下幾個部分:

  • 字節流
  • 字符流
  • 新 I/O

每個部分,都包含了輸入和輸出兩部分。

實現概要

這裏只給出每個類的實現概要,具體每個類的實現分析,可以參見我的 GitHub-SourceLearning-OpenJDK 頁面。根據導航中的鏈接,進入 java.io ,即可看到對每個類的分析。

字節流輸入

java_io_read_char

圖1 Java 字節輸入類

  • InputStream

InputStream 是所有字節輸入類的基類,它有一個未實現的 read 方法,子類需要實現這個 read 方法, 它和數據的來源相關。它的各種不同子類,或者是添加了功能,或者指明瞭不同的數據來源。

public abstract int read() throws IOException;
  • ByteArrayInputStream

ByteArrayInputStream 有一個內部 buffer , 包含從流中讀取的字節,還有一個內部 counter, 跟蹤下一個要讀入的字節。

protected byte buf[];
protected int pos;

這個類在初始化時,需要指定一個 byte[],作爲數據的來源,它的 read,就讀入這個 byte[] 中所包含的數據。

public ByteArrayInputStream(byte buf[]) {
    this.buf = buf;
    this.pos = 0;
    this.count = buf.length;
}
public synchronized int read() {
    return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
  • FileInputStream

FileInputStream 的數據來源是文件,即從文件中讀取字節。初始化時,需要指定一個文件:

public FileInputStream(File file) 
throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    if (name == null) {
        throw new NullPointerException();
    }
    fd = new FileDescriptor();
    fd.incrementAndGetUseCount();
    open(name);
}

以後讀取的數據,都來自於這個文件。這裏的 read 方法是一個 native 方法,它的實現與操作系統相關。

public native int read() throws IOException;
  • FilterInputStream

FilterInputStream將其它輸入流作爲數據來源,其子類可以在它的基礎上,對數據流添加新的功能。我們經常看到流之間的嵌套,以添加新的功能。就是在這個類的基礎上實現的。所以,它的初始化中,會指定一個字節輸入流:

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

讀取操作,就依靠這個流實現:

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

BufferedInputStream 是 FilterInputStream 的子類,所以,需要給它提供一個底層的流,用於讀取,而它本身,則爲此底層流增加功能,即緩衝功能。以減少讀取操作的開銷,提升效率。

protected volatile byte buf[];

內部緩衝區由一個 volatile byte 數組實現,大多線程環境下,一個線程向 volatile 數據類型中寫入的數據,會立即被其它線程看到。

read 操作會先看一下緩衝區裏的數據是否已經全部被讀取了,如果是,就調用底層流,填充緩衝區,再從緩衝區中按要求讀取指定的字節。

public synchronized int read() throws IOException {
    if (pos >= count) {
        fill();
        if (pos >= count)
            return -1;
    }
    return getBufIfOpen()[pos++] & 0xff;
}
private byte[] getBufIfOpen() throws IOException {
    byte[] buffer = buf;
    if (buffer == null)
        throw new IOException("Stream closed");
    return buffer;
}
  • DataInputStream

DataInputStream 也是 FilterInputStream 的子類,它提供的功能是:可以從底層的流中讀取基本數據類型,例如 intchar等等。DataInputStream 是非線程安全的, 你必須自己保證處理線程安全相關的細節。

例如,readBoolean 會讀入一個字節,然後根據是否爲0,返回 true/false

public final boolean readBoolean() throws IOException {
    int ch = in.read();
    if (ch < 0)
        throw new EOFException();
    return (ch != 0);
}

readShort 會讀入兩個字節,然後拼接成一個 short 類型的數據。

public final short readShort() throws IOException {
    int ch1 = in.read();
    int ch2 = in.read();
    if ((ch1 | ch2) < 0)
        throw new EOFException();
    return (short)((ch1 << 8) + (ch2 << 0));
}

int 和 long 依此類推,分別讀入4個字節,8個字節,然後進行拼接。

但是,浮點數就不能通過簡單的拼接來解決了,而要讀入足夠的字節數,然後再按照 IEEE 754 的標準進行解釋:

public final float readFloat() throws IOException {
    return Float.intBitsToFloat(readInt());
}
  • PushbackInputstream

PushbackInputstream 類也是FilterInputStream的子類,它提供的功能是,可以將已經讀入的字節,再放回輸入流中,下次讀取時,可以讀取到這個放回的字節。這在某些情境下是非常有用的。它的實現,就是依靠類似緩衝區的原理。被放回的字節,實際上是放在緩衝區裏,讀取時,先查看緩衝區裏有沒有字節,如果有就從這裏讀取,如果沒有,就從底層流裏讀取。

緩衝區是一個字節數組:

protected byte[] buf;

讀取時,優先從這裏讀取,讀不到,再從底層流讀取。

public int read() throws IOException {
    ensureOpen();
    if (pos < buf.length) {
        return buf[pos++] & 0xff;
    }
    return super.read();
}
  • PipedInputStream

PipedInputStream 與 PipedOutputStream 配合使用,它們通過 connect 函數相關聯。

public void connect(PipedOutputStream src) throws IOException {
    src.connect(this);
}

它們共用一個緩衝區,一個從中讀取,一個從中寫入。

PipedInputStream內部有一個緩衝區,

protected byte buffer[];

讀取時,就從這裏讀:

public synchronized int read()  throws IOException {
    if (!connected) {
        throw new IOException("Pipe not connected");
    } else if (closedByReader) {
        throw new IOException("Pipe closed");
    } else if (writeSide != null && !writeSide.isAlive()
               && !closedByWriter && (in < 0)) {
        throw new IOException("Write end dead");
    }

    readSide = Thread.currentThread();
    int trials = 2;
    while (in < 0) {
        if (closedByWriter) {
            /* closed by writer, return EOF */
            return -1;
        }
        if ((writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)) {
            throw new IOException("Pipe broken");
        }
        /* might be a writer waiting */
        notifyAll();
        try {
            wait(1000);
        } catch (InterruptedException ex) {
            throw new java.io.InterruptedIOException();
        }
    }
    int ret = buffer[out++] & 0xFF;
    if (out >= buffer.length) {
        out = 0;
    }
    if (in == out) {
        /* now empty */
        in = -1;
    }

    return ret;
}

過程比我們想的要複雜,因爲這涉及兩個線程,需要相互配合,所以,需要檢查很多東西,才能最終從緩衝區中讀到數據。

PipedOutputStream 類寫入時,會調用 PipedInputStream 的receive功能,把數據寫入 PipedInputStream 的緩衝區。

我們看一下 PipedOutputStream.write 函數:

public void write(int b)  throws IOException {
    if (sink == null) {
        throw new IOException("Pipe not connected");
    }
    sink.receive(b);
}

可以看出,調用了相關聯的管道輸入流的 receive 函數。

protected synchronized void receive(int b) throws IOException {
    checkStateForReceive();
    writeSide = Thread.currentThread();
    if (in == out)
        awaitSpace();
    if (in < 0) {
        in = 0;
        out = 0;
    }
    buffer[in++] = (byte)(b & 0xFF);
    if (in >= buffer.length) {
        in = 0;
    }
}

receive 的主要功能,就是把寫入的數據放入緩衝區內。

注意注意的是,這兩個類相互關聯的對象,應該屬於兩個不同的線程,否則,容易造成死鎖。

這個系列的第一部分到此結束,擴展閱讀部分的文章非常好,推薦閱讀。

擴展閱讀

發佈了91 篇原創文章 · 獲贊 235 · 訪問量 71萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章