Java I/O 22 - BufferedReader & BufferedWriter

  关于 java.io.BufferedReader java.io.BufferedWriter 的部分笔记,对字符读写提供了一个缓冲区作为中间媒介,以此降低读写操作与物理存储的交互次数并提高其读写效率和性能。本文演示代码段的执行环境基于JDK版本1.7

概述

  BufferedReader和BufferedWriter同BufferedInputStream和BufferedOutputStream一样,对输入输出提供了一套缓冲区机制,以此来提高数据读写的效率和性能。BufferedReader和BufferedWriter内部的缓冲区默认大小为8192,通过将数据存储在缓冲区中(本质上是存储在内存中)可以降低读写与物理存储介质的交互次数,以达到优化读写速度的目标。但是和BufferedInputStream和BufferedOutputStream不同的是,BufferedReader和BufferedWriter处理的字符流,其数据范围是0 ~ 65535,而BufferedInputStream和BufferedOutputStream处理的字节流。

继承关系

1
2
3
4
5
6
7
8
9
// BufferedReader
--java.lang.Object
--java.io.Writer
--java.io.BufferedReader

// BufferedWriter
--java.lang.Object
--java.io.Writer
--java.io.BufferedWriter

实现接口

类名 实现接口
BufferedReader Closeable, AutoCloseable,Readable
BufferedReader Closeable, AutoCloseable,Flushable, Appendable

BufferedReader

Constructor Summary

public BufferedReader(Reader in, int sz)

1
2
3
4
5
6
7
8
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;
}

  初始化一个字符输入流。底层缓存区buffer的长度由参数sz指定。nextChar指向下一个需要被读取的字符串位置,nChars标记缓冲区数组中存储的最后一个字符的位置下标。

public BufferedReader(Reader in)

1
2
3
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}

  初始化一个字符输入流。底层缓存区buffer的长度默认为8192。通过调用构造方法BufferedReader(Reader in, int sz)完成初始化操作。

部分方法

private void ensureOpen()

1
2
3
4
private void ensureOpen() throws IOException {
if (in == null)
throw new IOException("Stream closed");
}

  检查当前底层输出流是否开启且可用。

private void fill()

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
private void fill() throws IOException {
int dst;
if (markedChar <= UNMARKED) {
/* No mark */
dst = 0;
} else {
/* Marked */
int delta = nextChar - markedChar;
if (delta >= readAheadLimit) {
/* Gone past read-ahead limit: Invalidate mark */
markedChar = INVALIDATED;
readAheadLimit = 0;
dst = 0;
} else {
if (readAheadLimit <= cb.length) {
/* Shuffle in the current buffer */
System.arraycopy(cb, markedChar, cb, 0, delta);
markedChar = 0;
dst = delta;
} else {
/* Reallocate buffer to accommodate read-ahead limit */
char ncb[] = new char[readAheadLimit];
System.arraycopy(cb, markedChar, ncb, 0, delta);
cb = ncb;
markedChar = 0;
dst = delta;
}
nextChar = nChars = delta;
}
}

int n;
do {
n = in.read(cb, dst, cb.length - dst);
} while (n == 0);
if (n > 0) {
nChars = dst + n;
nextChar = dst;
}
}

  向当前底层缓冲区buffer中填充字符数据。在buffer中的数据都读完(即 nextChar >= nChars)时调用触发此方法。第2行代码定义了一个dst用来记录数据填充完成后下一个读取位置。第3 ~ 5行代码表示如果在填充之前没有调用过mark方法,那么会将缓冲区buffer的内容从头开始全部重新填充。

  第6 ~ 30行代码则处理在填充之前曾调用过mark方法的场景。其中,第8行代码用于计算最近一次mark方法产生的标记值到buffer结束的数据长度,如果得到的数据长度超过了允许的最大限度,那么将重新赋值并清除mark相关信息。反之,则将自mark标识位置起到buffer缓冲区结束的内容全部移动到缓冲区首部,释放尾部的空间以此容纳新读入的数据内容,如果需要的话,会适当对缓冲区buffer数组进行扩容处理(即第21 ~ 27行代码中的处理)。数据移动完成后,清除markedChar标记内容,同时更新下一个读取位置的具体位置信息(即dst)。第28行代码则更新了在完成数据移动后缓冲区buffer中下一个读取位置、buffer中存储内容的最后一个位置等信息。

  第32 ~ 35行代码会从底层输入流中读取新的字符内容,其长度足以全部填充缓冲区buffer的可用空间。读取执行结束后,更新nChars值为dst + n,理论上等于缓冲区buffer的长度。nextChar为下一个读取位置的值,需要注意的,如果执行了mark方法,那么这里返回的nextChar并不会重读起始位置,如果需要从标记重读位置开始读取数据,需要另外调用reset方法来更新nextChar值。

public int read()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int read() throws IOException {
synchronized (lock) {
ensureOpen();
for (;;) {
if (nextChar >= nChars) {
fill();
if (nextChar >= nChars)
return -1;
}
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '\n') {
nextChar++;
continue;
}
}
return cb[nextChar++];
}
}
}

  读取并返回一个字符内容,返回-1则表示文件已经读到了尾部。通过synchronized保证了多线程环境下的线程安全。第5 ~ 9行代码处理缓冲区buffer中已经没有内容可供读取需要从底层输入流中获取新的数据内容的场景。如果当前运行环境中指定了需要忽略换行符(skipLF = true),那么就跳过缓冲区buffer中存储的值为换行符的内容,读取下一个内容并执行相关的计算和判断。第17行代码则返回了缓冲区buffer中存储的一个字符内容给方法调用方。

public int read(char cbuf[], int off, int len)

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
public int read(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 0;
}

int n = read1(cbuf, off, len);
if (n <= 0) return n;
while ((n < len) && in.ready()) {
int n1 = read1(cbuf, off + n, len - n);
if (n1 <= 0) break;
n += n1;
}
return n;
}
}

private int read1(char[] cbuf, int off, int len) throws IOException {
if (nextChar >= nChars) {
/* If the requested length is at least as large as the buffer, and
if there is no mark/reset activity, and if line feeds are not
being skipped, do not bother to copy the characters into the
local buffer. In this way buffered streams will cascade
harmlessly. */
if (len >= cb.length && markedChar <= UNMARKED && !skipLF) {
return in.read(cbuf, off, len);
}
fill();
}
if (nextChar >= nChars) return -1;
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '\n') {
nextChar++;
if (nextChar >= nChars)
fill();
if (nextChar >= nChars)
return -1;
}
}
int n = Math.min(len, nChars - nextChar);
System.arraycopy(cb, nextChar, cbuf, off, n);
nextChar += n;
return n;
}

  读取并将读取的内容保存到字符数组cbuf并返回实际读取长度,返回-1则表示文件已经读到了尾部。通过synchronized保证了多线程环境下的线程安全。第4 ~ 9行代码对入参做了有效性校验,避免了溢出越界的发生。第11行代码则从底层buffer中获取数据,读取时会尽可能满足入参len规定的长度要求。如果读到了EOF标识,那么返回EOF表示已读完底层输入流的数据内容。如果长度要求没有达到且底层输入流仍旧可以提供数据,那么会继续读取数据(即第13 ~ 17行代码的操作流程)。最后返回实际读取的数据长度。

  整个读取过程调用的方法是private int read1(char[] cbuf, int off, int len)。在该方法中,如果缓冲区buffer中的内容已经读完,且如果入参len要求的长度超出了缓冲区的长度且未记录到打标信息且不容许忽略换行符,那么会直接从底层输入流中读取内容并返回,不会从缓冲区buffer中获取数据。如果缓冲区中的内容已经读完,那么会通过fill方法从底层输入流中获取数据填充到缓冲区buffer中。第34行代码如果在调用了fille方法进行缓冲区数据填充后还是没有可读取的数据,那么就认为底层输入流中已经没有可读取的数据了,所以返回EOF标识。第35 ~ 44行代码处理的需要忽略换行符的场景。如果缓冲区buffer中遇到了换行符\n,那么跳过该内容读取下一个内容,需要的时候可以调用fill方法向缓冲区buffer中填充新的数据内容或者返回EOF标识。

  第45行代码计算缓冲区buffer可以提供的数据容量,并将缓冲区buffer中的内容存储到字符数组cbuf中。之后更新nextChar的值并返回实际读取的字符内容长度。

public String readLine()

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
public String readLine() throws IOException {
return readLine(false);
}

String readLine(boolean ignoreLF) throws IOException {
StringBuffer s = null;
int startChar;

synchronized (lock) {
ensureOpen();
boolean omitLF = ignoreLF || skipLF;

bufferLoop:
for (;;) {

if (nextChar >= nChars)
fill();
if (nextChar >= nChars) { /* EOF */
if (s != null && s.length() > 0)
return s.toString();
else
return null;
}
boolean eol = false;
char c = 0;
int i;

/* Skip a leftover '\n', if necessary */
if (omitLF && (cb[nextChar] == '\n'))
nextChar++;
skipLF = false;
omitLF = false;

charLoop:
for (i = nextChar; i < nChars; i++) {
c = cb[i];
if ((c == '\n') || (c == '\r')) {
eol = true;
break charLoop;
}
}

startChar = nextChar;
nextChar = i;

if (eol) {
String str;
if (s == null) {
str = new String(cb, startChar, i - startChar);
} else {
s.append(cb, startChar, i - startChar);
str = s.toString();
}
nextChar++;
if (c == '\r') {
skipLF = true;
}
return str;
}

if (s == null)
s = new StringBuffer(defaultExpectedLineLength);
s.append(cb, startChar, i - startChar);
}
}
}

  读取一行内容并返回,每行内容由\n\r或者二者的组合分隔拆分得到。第16 ~ 23行代码中如果缓冲区buffer中的数据内容已经读取完毕,那么就调用fill()方法向buffer中填充新数据内容,如果底层输入流中已经到达了文件尾部,那么就以String形式当前已经得到的数据内容。第29行代码如果下一个读取位置存储的是一个换行符,那么跳过并读取下一个内容。第34 ~ 41行代码循环读取缓冲区buffer,获得两个换行符之间的完整数据内容。第46 ~ 59行代码处理的是成功读取了一行内容的场景:如果成功的读到了一行完整的内容(即eol = true),那么就从缓冲区buffer中取出这一行完整的内容并将之转换成一个String内容。更新维护nextChar的值并返回String字符串。如果当读取到缓冲区buffer尾部时尚未得到一行完整的内容(即eol = false),那么继续通过fill方法向缓冲区buffer填充新内容并执行上述计算过程。

public long skip(long n)

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
public long skip(long n) throws IOException {
if (n < 0L) {
throw new IllegalArgumentException("skip value is negative");
}
synchronized (lock) {
ensureOpen();
long r = n;
while (r > 0) {
if (nextChar >= nChars)
fill();
if (nextChar >= nChars) /* EOF */
break;
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '\n') {
nextChar++;
}
}
long d = nChars - nextChar;
if (r <= d) {
nextChar += r;
r = 0;
break;
}
else {
r -= d;
nextChar = nChars;
}
}
return n - r;
}
}

  跳过n个字符内容的长度,并从n+1位置开始读取字符内容。对n的要求是必须是一个大于等于0的数字。通过synchronized保证了多线程环境下的线程安全。第9 ~ 12行代码处理当前缓冲区buffer需要填充新数据内容的场景,且如果执行fill方法之后依旧没有新内容,那么会返回EOF标识给方法调用方。第13 ~ 18行代码处理需要忽略换行符的场景,如果遇到了换行符,那么就跳过该内容读取下一个内容。

  第19行代码计算缓冲区buffer中剩余的尚未读取的数据内容长度。如果入参跳过的长度n小于buffer剩余未读取的数据长度,那么会跳过缓冲区buffer中n个长度的内容并返回实际跳过长度给方法调用方。反之,则将缓冲区buffer中尚未读取的内容全部跳过,同时调用fill方法填充新的数据内容并继续计算需要跳过的长度直到跳过内容的总长度达到了n或者读到了文件结束位置。第30行代码计算最终跳过的长度并返回计算结果。

public boolean ready()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public boolean ready() throws IOException {
synchronized (lock) {
ensureOpen();

/*
* If newline needs to be skipped and the next char to be read
* is a newline character, then just skip it right away.
*/
if (skipLF) {
/* Note that in.ready() will return true if and only if the next
* read on the stream will not block.
*/
if (nextChar >= nChars && in.ready()) {
fill();
}
if (nextChar < nChars) {
if (cb[nextChar] == '\n')
nextChar++;
skipLF = false;
}
}
return (nextChar < nChars) || in.ready();
}
}

  通知方法调用方当前缓冲区buffer和底层输入流中的任意一个是否可以对外提供数据。通过synchronized保证了多线程环境下的线程安全。如果当前运行环境需要忽略换行符,首先在缓冲区buffer无未读取数据的情况下先从底层输入流中获取数据填充到缓冲区buffer中。之后判断下一个读取位置是否为换行符,如果是的话就跳过并读取下一个字符内容。通过计算缓冲区buffer中是否有尚未读取的数据内容,或者底层输入流是否可以继续提供数据来判断ready方法的返回值。

public boolean markSupported()

1
2
3
public boolean markSupported() {
return true;
}

  BufferedReader支持标记重读操作,所以返回true。

public void mark(int readAheadLimit)

1
2
3
4
5
6
7
8
9
10
11
public void mark(int readAheadLimit) throws IOException {
if (readAheadLimit < 0) {
throw new IllegalArgumentException("Read-ahead limit < 0");
}
synchronized (lock) {
ensureOpen();
this.readAheadLimit = readAheadLimit;
markedChar = nextChar;
markedSkipLF = skipLF;
}
}

  标记需要重读的数据内容的起点位置。通过synchronized保证了多线程环境下的线程安全。该方法执行过后,通过执行reset方法可以重新读取已经读取过的数据内容。调用mark方法需要readAheadLimit大于等于0。通过synchronized保证了多线程环境下的线程安全。方法内部会将当前数据的读取位置nextChar的值记录到markedChar中,同时维护是否需要忽略换行符的信息。

public void reset()

1
2
3
4
5
6
7
8
9
10
11
public void reset() throws IOException {
synchronized (lock) {
ensureOpen();
if (markedChar < 0)
throw new IOException((markedChar == INVALIDATED)
? "Mark invalid"
: "Stream not marked");
nextChar = markedChar;
skipLF = markedSkipLF;
}
}

  重置下一个读取位置实现数据重复读取场景。通过synchronized保证了多线程环境下的线程安全。在实现数据重读时,首先计算存储重读起始位置的markedChar是否有效。如果markedChar无效,那么会向上抛出异常,反之将nextChar的值更新为markedChar中维护的值,同时更新skipLF以完成所有操作。

public void close()

1
2
3
4
5
6
7
8
9
public void close() throws IOException {
synchronized (lock) {
if (in == null)
return;
in.close();
in = null;
cb = null;
}
}

  关闭当前输入流。通过使用synchronized关键字保证了多线程环境下的线程安全。如果底层输入流已经关闭,那么直接返回,否则通过调用底层输入流的close()方法关闭底层输入流并释放占用的相关资源。

BufferedWriter

Constructor Summary

public BufferedWriter(Writer out, int sz)

1
2
3
4
5
6
7
8
9
10
11
12
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"));
}

  初始化一个字符输出流。底层缓存区buffer的长度由参数sz指定。nextChar指向下一个保存字符内容的位置,nChars标记缓冲区数组中可以保存字符内容的最后一个位置。第10 ~ 11行代码初始化一个基于代码运行环境的行分隔符。

public BufferedWriter(Writer out)

1
2
3
public BufferedWriter(Writer out) {
this(out, defaultCharBufferSize);
}

  初始化一个字符输出流。底层缓存区buffer的长度默认为8192。通过调用构造方法BufferedWriter(Writer out, int sz)完成初始化操作。

部分方法

private void ensureOpen()

1
2
3
4
private void ensureOpen() throws IOException {
if (out == null)
throw new IOException("Stream closed");
}

  检查当前底层输出流是否开启且可用。

void flushBuffer()

1
2
3
4
5
6
7
8
9
void flushBuffer() throws IOException {
synchronized (lock) {
ensureOpen();
if (nextChar == 0)
return;
out.write(cb, 0, nextChar);
nextChar = 0;
}
}

  将当前缓冲区buffer中的内容推送到底层的目标输出流中。通过synchronized保证了多线程环境下的线程安全。如果缓冲区buffer中尚未存储任何内容(即nextChar == 0),那么不做任何操作直接返回。否则将缓冲区buffer中的内容推送到底层输出流中,同时将nextChar重新置为0。

public void write(int c)

1
2
3
4
5
6
7
8
public void write(int c) throws IOException {
synchronized (lock) {
ensureOpen();
if (nextChar >= nChars)
flushBuffer();
cb[nextChar++] = (char) c;
}
}

  向底层输出流写入一个字符内容(实际上是首先写入到了底层缓冲区buffer中)。通过synchronized保证了多线程环境下的线程安全。如果在写入前发现当前缓冲区buffer空间已满无法容纳新的内容,那么就调用flushBuffer()方法将缓冲区buffer中的内容推送到目标输出位置并释放缓冲区buffer的空间。之后将传入的字符数据c写入到缓冲区中保存。

public void write(char cbuf[], int off, int len)

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
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) {
/* If the request length exceeds the size of the output buffer,
flush the buffer and then write the data directly. In this
way buffered streams will cascade harmlessly. */
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();
}
}
}

private int min(int a, int b) {
if (a < b) return a;
return b;
}

  将字符数组cbuf中的内容写入到底层输出流中(实际上是首先写入到了底层缓冲区buffer中)。通过synchronized保证了多线程环境下的线程安全。第4 ~ 10行代码完成了参数的有效性校验,避免发生越界溢出。

  如果入参长度len大于缓冲区的容量,那么cbuf中的数据不会被保存到缓冲区中,而是首先将缓冲区buffer已有的数据推送到底层目标输出位置,然后直接把cbuf中的数据写入到底层目标输出位置里。反之,将cbuf中的数据先维护到缓冲区buffer中,如果buffer空间已满,则调用flushBuffer()方法将buffer中的数据推送到底层输出位置中,清空并释放buffer空间以此容纳新的数据,重复该过程直到cbuf中的内容全部被保存到了缓冲区buffer中(可能有部分数据已经被推送到了底层输出位置上)。

public void write(String s, int off, int len)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void write(String s, int off, int len) throws IOException {
synchronized (lock) {
ensureOpen();

int b = off, t = off + len;
while (b < t) {
int d = min(nChars - nextChar, t - b);
s.getChars(b, b + d, cb, nextChar);
b += d;
nextChar += d;
if (nextChar >= nChars)
flushBuffer();
}
}
}

  将字符串s中的内容写入到底层输出流中(实际上是首先写入到了底层缓冲区buffer中)。通过synchronized保证了多线程环境下的线程安全。第7行代码计算当前缓冲区buffer实际能容纳的s中的内容长度,第8行代码则将s中自off位置起,长度为d的内容复制保存到缓冲区buffer中。之后更新b、nextChar的值,如果缓冲区buffer空间已满则将缓冲区buffer中的内容推送到目标输出位置中。重复处理直到s中自off位置起长度为len的内容全部被保存到缓冲区buffer中(可能有部分数据已经被推送到了底层输出位置上)。

public void newLine()

1
2
3
public void newLine() throws IOException {
write(lineSeparator);
}

  写入一个换行符,实现换行效果。lineSeparator匹配当前运行环境机器的换行符格式。

public void flush()

1
2
3
4
5
6
public void flush() throws IOException {
synchronized (lock) {
flushBuffer();
out.flush();
}
}

  将当前缓冲区buffer中的内容推送到底层输出流。推送完毕后调用底层输出流的flush()方法将数据真正推送到目标输出位置。

public void close()

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

  关闭当前输出流。通过使用synchronized关键字保证了多线程环境下的线程安全。如果底层输出流已经关闭,那么直接返回,否则首先将缓冲区buffer中的数据推送到目标输出位置,然后调用底层输出流的close()方法关闭底层输出流并释放占用的相关资源。

涉及基础知识点

  1. NIL

参考文献

  1. NIL




------------- End of this article, thanks! -------------


  版权声明:本文由N.C.Lee创作和发表,采用署名(BY)-非商业性使用(NC)-相同方式共享(SA)国际许可协议进行许可,转载请注明作者及出处。
  本文作者为 N.C.Lee
  本文标题为 Java I/O 22 - BufferedReader & BufferedWriter
  本文链接为 https://marcuseddie.github.io/2018/java-BufferedReader-BufferedWriter.html