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 類圖
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;
}
每天用心記錄一點點。內容也許不重要,但習慣很重要!