Java.io之BufferedWriter

通常,Writer会立即将数据发送到其绑定的字符流或字节流。一般建议使用BufferedWriter对任何write()操作代价高的Writer进行包装,例如FileWriters和OutputStreamWriters。例如,将PrintWriter的输出存放到文件。如果没有缓冲,每次调用print()方法都会导致字符转换为字节,然后立即写入文件,这可能效率很低。

1
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));

BufferedWriter的源码比较简单,下文直接做对代码解析

BufferedWriter字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package java.io;

public class BufferedWriter extends Writer {

//被BufferedWriter包装的Writer
private Writer out;
//缓冲区
private char cb[];
//缓冲区中字符数,下一个输出的字符位置
private int nChars, nextChar;
//默认缓冲区大小
private static int defaultCharBufferSize = 8192;
//平台相关的行分隔符
private String lineSeparator;

//默认缓冲区大小 out为被BufferedWriter包装的Writer
public BufferedWriter(Writer out) {
this(out, defaultCharBufferSize);
}

//指定缓冲区大小的BufferedWriter
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"));
}

//检查输出流是否已经关闭
private void ensureOpen() throws IOException {
if (out == null)
throw new IOException("Stream closed");
}

//将输出缓冲区刷新到绑定的字符流,而不刷新流本身。
void flushBuffer() throws IOException {
synchronized (lock) {
ensureOpen();
if (nextChar == 0)
return;
out.write(cb, 0, nextChar);
nextChar = 0;
}
}

//像缓冲区中写入一个字符的int值 若缓冲区满则调用flushBuffer()
public void write(int c) throws IOException {
synchronized (lock) {
ensureOpen();
if (nextChar >= nChars)
flushBuffer();
cb[nextChar++] = (char) c;
}
}

//BufferedWriter类的工具方法 避免调用Math.min()
//至于这样做的原因 见<讨论>
private int min(int a, int b) {
if (a < b) return a;
return b;
}

//将从cbuf[off]到cbuf[off+len-1]的字符写入缓冲区
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;
}
//字符数整个缓冲区都装不下的情况
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();
}
}
}

//将字符串s从charAt(off)开始的len个字符写入缓冲区
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();
}
}

//关闭BufferedWriter并flushBuffer()
public void close() throws IOException {
synchronized (lock) {
if (out == null) {
return;
}
try (Writer w = out) {
flushBuffer();
} finally {
out = null;
cb = null;
}
}
}
}

讨论

堆栈追踪(stack trace)

参考阅读

1
2
3
4
private int min(int a, int b) {
if (a < b) return a;
return b;
}

JVM使用file descriptor指定文件,当打印堆栈跟踪(stack trace)时文件描述符资源用光了,如果你调用java.lang.Math.min(),由于没有文件描述符资源,类加载器不能加载Math类,那甚至无法打印堆栈跟踪。因此BufferedWriter写了自己的min()方法避免加载Math.min().

try-with-resource语法

1
2
3
4
5
6
7
8
9
10
11
12
13
public void close() throws IOException {
synchronized (lock) {
if (out == null) {
return;
}
try (Writer w = out) {
flushBuffer();
} finally {
out = null;
cb = null;
}
}
}

参考阅读:知乎-斯巴拉西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//所有实现Closeable的类声明都可以写在里面,不用写一大堆finally来关闭资源
//最常见于流操作,socket操作,新版的httpclient也可以;
//需要注意的是,try()的括号中可以写多行声明,每个声明的变量类型都必须是Closeable的子类,用分号隔开.
InputStream is = null;
OutputStream os = null;
try {
//...
} catch (IOException e) {
//...
}finally{
try {
if(os!=null){
os.close();
}
if(is!=null){
is.close();
}
} catch (IOException e2) {
//...
}
}

//而现在你可以这样写:
try(
InputStream is = new FileInputStream("...");
OutputStream os = new FileOutputStream("...");
){
//...
}catch (IOException e) {
//...
}

flushBuffer()与flush()的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//将输出缓冲区刷新到绑定的字符流,而不刷新流本身。
void flushBuffer() throws IOException {
synchronized (lock) {
ensureOpen();
if (nextChar == 0)
return;
out.write(cb, 0, nextChar);
nextChar = 0;
}
}

//将缓冲区字符刷到字符流中并把字符流中数据刷到输出流的目的地
public void flush() throws IOException {
synchronized (lock) {
flushBuffer();
out.flush();
}
}

可以看到flush()中先调用了flushBuffer()然后调用了out.flush(),所以flush()是彻底的,其将缓冲区中数据刷到输出流目的地中,而flushBuffer()调用out.write()仅将字符刷到BufferedWriter包装的Writer中。

Donate here.