Java 字符編碼(三)Reader 中的編解碼


Java 字符編碼(三)Reader 中的編解碼

我們知道 BufferedReader 可以將字節流轉化為字符流,那它是如何編解碼的呢?

try (BufferedReader reader = new BufferedReader(new FileReader(...));) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

一、Reader

1.1 Reader

Reader 中有四個重載的 read 方法:

// 讀到 CharBuffer 中
public int read(java.nio.CharBuffer target) throws IOException {
    int len = target.remaining();
    char[] cbuf = new char[len];
    int n = read(cbuf, 0, len);
    if (n > 0)
        target.put(cbuf, 0, n);
    return n;
}

// 讀一個字符
public int read() throws IOException {
    char cb[] = new char[1];
    if (read(cb, 0, 1) == -1)
        return -1;
    else
        return cb[0];
}
// 讀多個字符
public int read(char cbuf[]) throws IOException {
    return read(cbuf, 0, cbuf.length);
}

// 由子類實現
public abstract int read(char cbuf[], int off, int len) throws IOException;

1.2 Reader 類圖

Reader 類圖

BufferedReader -> InputStreamReader -> StreamDecoder -> InputStream。真正處理編解碼的是 StreamDecoder 類。

二、StreamDecoder

《StreamDecoder流源碼》:https://blog.csdn.net/ai_bao_zi/article/details/81205286

2.1 read()方法

返回讀取的一個字符,當讀到文件末尾時返回 -1。

public int read() throws IOException {
    return read0();
}

// read0 每次會讀取 2 個字符,但 read0 只會返回一個字符
// 因此會將多讀的另一個字符先存儲在 leftoverChar 中
private int read0() throws IOException {
    synchronized (lock) {

        // 1. 如果上次讀的兩個字符還剩下的一個未返回,直接返回即可
        if (haveLeftoverChar) {
            haveLeftoverChar = false;
            return leftoverChar;
        }

        // 2. 每次讀取兩個字符,返回第一個字符,另一個存儲在 leftoverChar 中
        char cb[] = new char[2];
        int n = read(cb, 0, 2);
        switch (n) {
        // 2.1 文件流已讀完,直接返回
        case -1:
            return -1;
        // 2.2 如果讀取到 2 個字符,則返回第一個,緩存第二個
        case 2:
            leftoverChar = cb[1];
            haveLeftoverChar = true;
            // FALL THROUGH
        case 1:
            return cb[0];
        default:
            assert false : n;
            return -1;
        }
    }
}

read0 方法每次讀 2 個字符,為什么不是 1 個或者多個?首先多個可以使用 read(char cbuf[], int offset, int length) 方法,其次當實際要讀取的字符為 len=1 時會直接調用 read0 方法,即回調 read(cb[], 0, 2)。

2.2 read(char cbuf[], int offset, int length)方法

該方法最多讀取 length 個字節放入字符數組中,從字符數組的偏移量 offset 開始存儲,返回實際讀取存儲的字節數,當讀取到文件末尾時,返回 -1。

public int read(char cbuf[], int offset, int length) throws IOException {
    int off = offset;
    int len = length;
    synchronized (lock) {
        ensureOpen();
        if ((off < 0) || (off > cbuf.length) || (len < 0) ||
            ((off + len) > cbuf.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        }
        if (len == 0)
            return 0;

        int n = 0;
        // 1. 首先取出 leftoverChar
        if (haveLeftoverChar) {
            // Copy the leftover char into the buffer
            cbuf[off] = leftoverChar;
            off++; len--;
            haveLeftoverChar = false;
            n = 1;
            if ((len == 0) || !implReady())
                // Return now if this is all we can produce w/o blocking
                return n;
        }

        // 2. 只讀取一個則直接調用 read0 方法,即回調 read(cb[], 0, 2),如果 read0 的 length=1 會循環遞歸
        //    這時 length=2 不會進入 if 分支,直接調用 implRead 方法
        if (len == 1) {
            // Treat single-character array reads just like read()
            int c = read0();
            if (c == -1)
                return (n == 0) ? -1 : n;
            cbuf[off] = (char)c;
            return n + 1;
        }

        // 3. implRead 真正用於讀取字節流到字符流 cbuf 中,返回實際讀取的字符數
        return n + implRead(cbuf, off, off + len);
    }
}

2.3 implRead(cbuf, off, end)

讀取字符到數組中,從數組的偏移量 offset 開始存儲,最多存儲到偏移量 end,返回實際讀取存儲的字符個數。

int implRead(char[] cbuf, int off, int end) throws IOException {
    // 1. 每次最少讀取 2 個字符
    assert (end - off > 1);

	//2. 將字符數組包裝到緩沖區中,緩沖區修改,字符數組也會被修改
	//   cb 本質理解為一個數組,當前位置為 off,界限為 end-off
    CharBuffer cb = CharBuffer.wrap(cbuf, off, end - off);
    if (cb.position() != 0)
	    // Ensure that cb[0] == cbuf[off]
	    // slice 不會修改 cbuf 字符數組,只修改了 cb 指針位置,即忽略了 cubf[] 中已經有的字符
	    cb = cb.slice();

	// 3. 將 readBytes 讀到緩沖區 bb 中的字節解碼到 cb 中
    boolean eof = false;
    for (;;) {
    	// 3.1 將字節緩沖區 bb 中解碼到字符緩沖區 cb 中
	    CoderResult cr = decoder.decode(bb, cb, eof);
	    // 3.2 解碼成功
	    if (cr.isUnderflow()) {
	    	// 流中數據讀取完畢或 cb 沒有空間了就直接返回
	        if (eof)
	            break;
	        if (!cb.hasRemaining())
	            break;
	        // 如果流不能讀取而 cb 已經有部分解碼成功就直接返回,否則調用 readBytes 等待流的讀取
	        if ((cb.position() > 0) && !inReady())
	            break;          // Block at most once
	        // 從流中讀取數據到 bb 中,如果 n<0 則數據讀取完畢,但 bb 中還有數據的話會盡量再進行一次解碼
	        int n = readBytes();
	        if (n < 0) {
	            eof = true;
	            if ((cb.position() == 0) && (!bb.hasRemaining()))
	                break;
	            decoder.reset();
	        }
	        continue;
	    }
	    // 3.3 cb 中沒有空間了,返回由上層擴容處理
	    if (cr.isOverflow()) {
	        assert cb.position() > 0;
	        break;
	    }
	    // 3.4 解碼異常
	    cr.throwException();
    }

    // 4. 清空 decoder 狀態
    if (eof) {
	    // ## Need to flush decoder
	    decoder.reset();
    }

    // 4. 返回讀取的字節數
    if (cb.position() == 0) {
        if (eof)
            return -1;
        assert false;
    }
    return cb.position();
}

implRead 調用結束的的條件:一是流讀取完畢;二是 cb 沒有空間了,也就是達到了要讀取的字符數。否則就會調用 readBytes 將數據中流讀到 bb 中一直進行解碼。

2.4 readBytes()

利用字節輸入流嘗試讀取最多 8192 個字節到字節緩沖區中,此方法是核心點:讀取字節到字節緩沖區才可以利用編碼器編碼字節成字符。

readBytes 方法真正與底層的流打交道,與之相關的屬性如下:

// cs、decoder 字符集
private Charset cs;
private CharsetDecoder decoder;
// 從字節流中讀取出的緩沖區,用於解碼
private ByteBuffer bb;

// 可能為 bio 也可能為 nio,有且僅有一個字段不為空,只能選一個
private InputStream in;
private ReadableByteChannel ch;
private int readBytes() throws IOException {
	// compact 丟棄了 position 之前的字節,這些字節已經解碼完畢,可以丟棄
    bb.compact();
    try {
    	// 從 nio 中讀取
	    if (ch != null) {
	        // Read from the channel
	        int n = ch.read(bb);
	        if (n < 0)
	            return n;
	    } else {
	        // 從 bio 中讀取
	        int lim = bb.limit();
	        int pos = bb.position();
	        assert (pos <= lim);
	        int rem = (pos <= lim ? lim - pos : 0);
	        assert rem > 0;
	        int n = in.read(bb.array(), bb.arrayOffset() + pos, rem);
	        if (n < 0)
	            return n;
	        if (n == 0)
	            throw new IOException("Underlying input stream returned zero bytes");
	        assert (n <= rem) : "n = " + n + ", rem = " + rem;
	        bb.position(pos + n);
	    }
    } finally {
	    // Flip even when an IOException is thrown,
	    // otherwise the stream will stutter
	    bb.flip();
    }

    // 返回可以使用的字節數
    int rem = bb.remaining();
    assert (rem != 0) : rem;
    return rem;
}

2.5 StreamDecoder 是如何保證數據流中的每一個字節都按順序解碼的呢?

以 sun.nio.cs.UTF_8 為例,這個類繼承了 Charset,有兩個內部類 Decoder 和 Encoder。每次解碼完成后才會更新 Buffer 對應的字節,UTF_8#updatePositions 代碼如下:

// 初始值: sp = src.arrayOffset() + src.position(); 每讀處理一個字節 sp 都會遞增
// 更新 src 和 dst 的實際 position 值
private static final void updatePositions(Buffer src, int sp,
        Buffer dst, int dp) {
    src.position(sp - src.arrayOffset());
    dst.position(dp - dst.arrayOffset());
}

而每次重新讀取數據前 StreamDecoder#readBytes 都會丟棄之前已經處理好的字節,這樣就不會重復解碼:

private int readBytes() throws IOException {
    // compact 丟棄了 position 之前的字節,這些字節已經解碼完畢,可以丟棄
    bb.compact();
    ...
}

這樣 StreamDecoder#readBytes 每次讀取數據前調用 Buffer#compact 壓縮 position 之前的數據,而 UTF_8#decodeLoop 解碼完成后都會調用 UTF_8#updatePositions 更新字節碼的 position 位置。

public ByteBuffer compact() {
    // ix = offset + position()
    System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
    position(remaining());
    limit(capacity());
    discardMark();
    return this;
}

每天用心記錄一點點。內容也許不重要,但習慣很重要!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM