今天学习BufferedReader与BufferedWriter。
BufferedReader,字符缓冲输入流,作用是为其他输入流提供缓冲功能。BufferedReader从其他字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
通常,Reader所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。因此,建议用BufferedReader包装所有其read()操作可能开销很高的Reader(如FileReader和InputStreamReader)。例如,
BufferedReader in = new BufferedReader(new FileReader("foo.in"));
将缓冲指定文件的输入。如果没有缓冲,则每次调用read()或readLine()都会导致从文件中读取字节,并将其转换为字符后返回,而这是极其低效的。
BufferedWriter,字符缓冲输出流,作用是为其他输出流提供缓冲功能。BufferedWriter将文本写入其他字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。
通常Writer将其输出立即发送到底层字符或字节流。除非要求提示输出,否则建议用BufferedWriter包装所有其write()操作可能开销很高的 Writer(如FileWriters和OutputStreamWriters)。例如,PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));
将缓冲PrintWriter对文件的输出。如果没有缓冲,则每次调用print()方法会导致将字符转换为字节,然后立即写入到文件,而这是极其低效的。
BufferedReader
为了更好地理解BufferedReader的源码,我们先来了解其思想。BufferedReader是如何为其他输入流提供缓冲功能的?提供缓冲为什么能实现字符、数组和行的高效读取?
先来看看第一次问题,BufferedReader是如何为其他输入流提供缓冲功能的?
创建BufferReader时,我们会通过它的构造函数指定某个Reader为参数。BufferedReader将Reader中的数据分批取到自己的buffer中(内置缓存字符数组),然后处理buffer中的数据。操作完buffer中的数据后,BufferedReader会从Reader中读取下一批数据到buffer中供程序处理。如此循环往复,直到Reader中数据被读取完毕。这其中就有个关键的方法fill(),每当buffer中数据被读取完之后,fill()就会从Reader中将数据填充到buffer中。这个方法下面会仔细讲解。
提供缓冲为什么能实现字符、数组和行的高效读取?
一是提高了读取的效率、二是减少了打开存储介质的连接次数。缓冲中的数据实际上是保存在内存中,而原始数据可能是保存在硬盘中。从内存中读取数据的速度比从硬盘读取数据的速度至少快10倍以上。
到这里又有个问题,为什么不一次性将Reader中全部数据都读取到缓冲中呢?
第一,读取全部的数据所需要的时间可能会很长。第二,内存价格很贵,容量远没有硬盘那么大。我们能做的就是在效率和成本之间找到平衡点。大多数情况下,默认值就足够大了。
下面来学习下BufferedReader的源码,看看BufferedReader是如何为其他输入流提供缓冲功能,实现字符、数组和行的高效读取的。
public class BufferedReader extends Reader {
// 底层字符输入流
private Reader in;
// 字符缓冲区
private char cb[];
// nChars是cb字符缓冲区中字符的总的个数
// nextChar是下一个要读取的字符在cb缓冲区中的位置
private int nChars, nextChar;
// 表示标记无效。设置了标记,但是被标记位置由于某种原因导致标记无效
private static final int INVALIDATED = -2;
//表示没有标记
private static final int UNMARKED = -1;
//标记位置初始化为UNMARKED
private int markedChar = UNMARKED;
//在仍保留该标记的情况下,对可读取字符数量的限制。
//在读取达到或超过此限制的字符后,尝试重置流可能会失败。
//限制值大于输入缓冲区的大小将导致分配一个新缓冲区,其大小不小于该限制值。因此应该小心使用较大的值。
private int readAheadLimit = 0; /* Valid only when markedChar > 0 */
//表示是否跳过换行符。(skipLF ,skip line feed)
private boolean skipLF = false;
//表示当做了标记时,是否忽略换行符。
private boolean markedSkipLF = false;
//字符缓冲区默认大小
private static int defaultCharBufferSize = 8192;
//每行默认的字符个数
private static int defaultExpectedLineLength = 80;
/**
* 创建指定底层字符输入流in和指定字符缓冲区大小sz的BufferedReader
*/
public BufferedReader(Reader in, int sz) {
super(in);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.in = in;
cb = new char[sz];
nextChar = nChars = 0;
}
/**
* 创建指定底层字符输入流in和默认字符缓冲区大小defaultCharBufferSize的BufferedReader
*/
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
/**
* 检查BufferedReader是否处于open状态。
* 如果in不为null,BufferedReader即为open状态。
*/
private void ensureOpen() throws IOException {
if (in == null)
throw new IOException("Stream closed");
}
/**
* 填充缓冲区。
* 如果标记有效,要考虑标记。
*/
private void fill() throws IOException {
//cb中填充数据的起始位置
int dst;
//如果没有标记,从缓冲区索引为0的位置开始填充
if (markedChar <= UNMARKED) {
dst = 0;
} else {
//如果有标记
//delta为标记位置与下个读取字符之间的距离
int delta = nextChar - markedChar;
//如果delta超出readAheadLimit,标记即为无效。
if (delta >= readAheadLimit) {
markedChar = INVALIDATED;
readAheadLimit = 0;
dst = 0;
} else {
//如果delta没有超出readAheadLimit,即标记有效
//且readAheadLimit小于等于缓冲区长度
//将markedChar与nextChar之间的字符写入到缓冲区中
if (readAheadLimit <= cb.length) {
/* Shuffle in the current buffer */
System.arraycopy(cb, markedChar, cb, 0, delta);
markedChar = 0;
dst = delta;
} else {
//如果delta没有超出readAheadLimit,即标记有效
//且readAheadLimit大于缓冲区长度
//将重新设置缓冲区大小,markedChar与nextChar之间的字符写入到缓冲区中
char ncb[] = new char[readAheadLimit];
System.arraycopy(cb, markedChar, ncb, 0, delta);
cb = ncb;
markedChar = 0;
dst = delta;
}
nextChar = nChars = delta;
}
}
int n;
//从底层输入流中读取数据,并存储到缓冲区b中
//如果没有读取到数据,就继续读,直到读到数据或者到达流末尾为止
do {
n = in.read(cb, dst, cb.length - dst);
} while (n == 0);
//如果读到了数据
if (n > 0) {
nChars = dst + n;
nextChar = dst;
}
}
/**
* 读取单个字符。
*
* @return 作为一个整数(其范围从0到65535( 0x00-0xffff))返回,如果已到达流末尾,则返回 -1
*/
public