一、BufferedWriter介绍
BufferedWriter继承自Writer类是字符缓冲输出流,它通过在内部创建一个字符缓冲区(char数组)为底层绑定的其他字符输出流Writer提供缓冲的功能,在不要求字符数据即时写入情况下可以实现单个字符、数组、字符串的高效写入。他提供字符、数组等高效写入的原理:若不在内部提供一个缓冲区,那么每次写入操作都要直接操作底层字符输出流Writer将字符转为字节数据然后向目的文件或者其他目标写入数据每写入一次都要先建立连接,访问磁盘,无疑效率是非常低下的,BufferedWriter通过在内部创建一个字符缓冲区,每次写入的时候先保存到内部缓冲区中,当到达一定数量时再将缓冲区中的字符数据一次性写入到绑定的底层字符输出流中,这样虽然不能实现数据实时写入到磁盘但提升了效率减少了磁盘访问次数。
二、BufferedWriter成员变量
public class BufferedWriter extends Writer {
//底层绑定的字符输出流
private Writer out;
//内部字符数据缓冲区
private char cb[];
//缓冲区大小
private int nChars;
//缓冲区下一个字符写入位置
int nextChar;
//默认缓冲区大小
private static int defaultCharBufferSize = 8192;
//行分隔符
private String lineSeparator;
}
三、BufferedWriter源码分析
1 - 构造函数
/**
* 构造函数,指定绑定的底层字符输出流,创建一个默认大小的字符输出缓冲区
*/
public BufferedWriter(Writer out) {
this(out, defaultCharBufferSize);
}
/**
* 构造函数,指定绑定的底层字符输出流,创建指定大小的字符输出缓冲区
*/
public BufferedWriter(Writer out, int sz) {
super(out);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.out = out;
cb = new char[sz];
nChars = sz;
nextChar = 0;
lineSeparator = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("line.separator"));
}
由于第一个构造方法内部调用的其实也是第二个构造方法,我们直接分析下第二个构造方法BufferedWriter(Writer out, int sz),分析源码该方法内部首先调用了父类构造方法super(out)该方法将BufferedWrite的锁对象引用lock指向方法指定的底层字符输出流对象out,接着对方法的缓冲区大小参数sz进行合法性校验,若小于0抛出异常,接下来就是绑定底层字符输出流out,创建指定长度sz缓冲区数组,初始化缓冲区下一个字符写入位置nextChar和缓冲区大小naChars。
2 -void write(int c)
public void write(int c) throws IOException {
synchronized (lock) {
//检查流状态
ensureOpen();
//缓冲区是否写满,若满则刷新缓冲区
if (nextChar >= nChars)
flushBuffer();
//在缓冲区数组写入字符
cb[nextChar++] = (char) c;
}
}
这里方法内部首先调用ensureOpen方法检测流是否已经关闭,若未关闭判断缓冲区是否写满,若已经写满则调用flushBuffer方法刷新缓冲区,我们进入该方法源码看下实现:
void flushBuffer() throws IOException {
synchronized (lock) {
ensureOpen();
if (nextChar == 0)
return;
out.write(cb, 0, nextChar);
nextChar = 0;
}
}
flushBuffer方法判断了nextChar是否为0若为0则说明缓冲区还没有写入过或者之前已经刷新过流但还没写入数据,否则调用out.write方法将当前字符缓冲区数据写入到底层字符输出流out中重置缓冲区下一个字符写入位置nextChar为0,重复利用缓冲区,覆盖缓冲区原有的字符数据。
分析完flushBuffer方法再回到调用方write方法继续往下分析,最后将指定字符插入缓冲区数组cb的当前写入位置。
总结write(int c)方法的基础逻辑是首先判断BufferedWriter的流状态,若流已经关闭(即底层字符输出流为null)则抛出异常,否则判断缓冲区是否写满若写满则将缓冲区字符数据写入到底层字符输出流后重置缓冲区重新从字符缓冲区数组开头写入字符数据,最后将指定字符c写入缓冲区。
3 - 其他成员方法
/** 检测BufferedWriter是否关闭 **/
private void ensureOpen() throws IOException {
if (out == null)
throw new IOException("Stream closed");
}
/**
* 刷新内部的字符输出缓冲区,将缓冲区字符数据写入底层输出流中,并重置缓冲区下一个写入字符位置nextChar
*/
void flushBuffer() throws IOException {
synchronized (lock) {
ensureOpen();
if (nextChar == 0)
return;
out.write(cb, 0, nextChar);
nextChar = 0;
}
}
/**
* 取最小值
*/
private int min(int a, int b) {
if (a < b) return a;
return b;
}
/**
* 写入字符数组的一部分,从下标off开始的len个字符
*/
public void write(char cbuf[], int off, int len) throws IOException {
synchronized (lock) {
//检测流的状态
ensureOpen();
//校验参数的合法性
if ((off < 0) || (off > cbuf.length) || (len < 0) ||
((off + len) > cbuf.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
//若字符数组写入的字符个数len大于缓冲区大小,刷新当前缓冲区将缓冲区字符数据写入到底层字符输出流,重置缓
//冲区,之后将要写入的字符数组数据直接写入底层字符输出流
if (len >= nChars) {
flushBuffer();
out.write(cbuf, off, len);
return;
}
//若写入的字符个数小于缓冲区大小则将数据先写入缓冲区中,在缓冲区写满时将数据写入底层字节输出流,然后重置
//缓冲区,继续写入
int b = off, t = off + len;
while (b < t) {
int d = min(nChars - nextChar, t - b);
System.arraycopy(cbuf, b, cb, nextChar, d);
b += d;
nextChar += d;
if (nextChar >= nChars)
flushBuffer();
}
}
}
/**
* 写入一个字符串的一部分,从下标off开始的len个字符写入到内部字符缓冲区cb中
*/
public void write(String s, int off, int len) throws IOException {
synchronized (lock) {
ensureOpen();
int b = off, t = off + len;
while (b < t) {
//获取缓冲区剩余写入空间和写入字符个数len的较小值
int d = min(nChars - nextChar, t - b);
//将字符串方法参数指定部分写入到缓冲区
s.getChars(b, b + d, cb, nextChar);
b += d;
nextChar += d;
//判断缓冲区是否写满,若写满刷新缓冲区字符数据到底层字符输出流,重置缓冲区后继续写入直到写完
if (nextChar >= nChars)
flushBuffer();
}
}
}
/**
* 写入一个行分隔符
*/
public void newLine() throws IOException {
write(lineSeparator);
}
/**
* 刷新流,刷新缓冲区数据将其写入底层字符输出流中
*/
public void flush() throws IOException {
synchronized (lock) {
flushBuffer();
out.flush();
}
}
/**
* 关闭流,主要是将刷新缓冲区将未写入底层字符输出流的数据写入底层字符输出流,然后将底层字符输出流和缓冲区设置为
* null释放对象资源
*/
public void close() throws IOException {
synchronized (lock) {
if (out == null) {
return;
}
try (Writer w = out) {
flushBuffer();
} finally {
out = null;
cb = null;
}
}
}