字符流输入 FileReader
读FileReader的时候,结合BuffedReader一起看。参考前一篇文章FileReader。
先说结论
BufferedReader 封装了FileReader和字符缓冲区这两个关键内容。
BufferedReader通过FileReader从文件中获取字节流并decode解码成字符数组,存储再字符缓冲去。
BufferedReader通过字符缓冲区给调用方获取指定位置的字符。
类图
FileReader继承了InputStreamReader。
InputStreamReader的filed sd是StreamDecoder。
StreamDecoder有几类属性:
1、用于存储从in InputStream或者ch ReadableByteChannel读取的字节数组的bb 字节缓冲区
2、decoder,用于解码字节数组的解码器。decoder是CharsetDeoder实力,因为字符编解码有很多,包括UTF_8。类图以UTF_8为例讲解。当编码方式选择UTF_8的时候,decoder就是一个UTF_8编码器实例;
关键方法
FileReader.read()
FileReader.java
public int read() throws IOException {
//代理了一下StreamDecoder.reader()
return sd.read();
}
StreamDecoder.java
public int read() throws IOException {
return read0();
}
@SuppressWarnings("fallthrough")
private int read0() throws IOException {
synchronized (lock) {
// Return the leftover char, if there is one
if (haveLeftoverChar) {
haveLeftoverChar = false;
return leftoverChar;
}
// Convert more bytes
//cb是一个长度为2的字符数组,当第一次调用read0的时候,读取的是cb的第一个字符,当第二次调用read0方法的时候
//读取的是第二个字符,而读取第一/第二个字符就是靠haveLeftoverChar和leftoverChar实现的。
//读取第一个字符后,havaleftoverChar变为true,leftoverChar存储cb[1],也就是存储第二个字符。
char cb[] = new char[2];
int n = read(cb, 0, 2);
switch (n) {
case -1:
return -1;
case 2:
leftoverChar = cb[1];
haveLeftoverChar = true;
// FALL THROUGH
case 1:
return cb[0];
default:
assert false : n;
return -1;
}
}
}
注意看StreamDecoder.java中的read0方法。
cb是一个长度为2的字符数组,当第一次调用read0的时候,读取的是cb的第一个字符,当第二次调用read0方法的时候
读取的是第二个字符,而读取第一/第二个字符就是靠haveLeftoverChar和leftoverChar实现的。
读取第一个字符后,havaleftoverChar变为true,leftoverChar存储cb[1],也就是存储第二个字符。
总结一下:
使用FileReader的逐字符读取的时候,实际上FileReader相当于2个字符的缓冲区数组不断读取,读取完成后又调用decoder对bb字节数组进行字符解码。解码详细解读参考FileReader讲解。
FileReader.read(char cbuf[], int offset, int length)
FileReader.java
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
StreamDecoder.java
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;
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;
}
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;
}
return n + implRead(cbuf, off, off + len);
}
}
可以看到,当len=1的时候,就相当于read()方法,逐字读取。
当len不是1的时候,执行implRead(cbuf, off, off+len)方法。这个方法实际上就是decode解码字节缓冲区bb,将解码到的字符存储在cbuf数组中。
比较一下FileReader和BufferedReader
本质上都是通过StreamDecoder对文件进行一次io,读取8192个字节存储到bb字节缓冲区,之后通过解码器进行CPU计算出来要取的指定个数的字符。
而BufferedReader使用了默认长度为8192的字符缓冲区来存储解码后的字符数据。
假如一个文件中有8192个中文字符“你”,我们知道一个“你”,用过UTF_8编码,需要3个字节。
那么如果通过BufferedReader读取完所有字符。有这么几个过程:
1、StreamDecoder一次文件io,读取8192个字节到bb缓冲区,之后通过UTF_8解码器将字节解码为字符,可以解码2730个字符。然后将2730个字符存储到字符缓冲区,而因为剩下的2个字节不足以完成一个字符的解码,所以保留最后这两个字节。
2、StreamDecoder第二次文件io,读取8190个字节,加上第一步剩下的的2个字节,总共还是8192个字节,继续进行解码。
不断往复累计进行4次,即可完成BufferedReader的8192个字符的缓冲区的填充。
如果使用FileReader的效果是什么?
如果调用FileReader的read(char cbuf[], int offset, int length) 方法,offset=0,length=8192;那么实际上和上面BufferedReader的效果是类似的。包括文件交互的io数。
但是如果是逐字符读取,那每次都要调用解码器解码的次数多很多。