Java
Java IO

Java IO 23 - BufferedReader详解

简介:流是一组有顺序的,有起点和终点的字节集合。是对设备文件间数据传输的总称和抽象。

1. BufferedReader介绍

BufferedReader是缓冲字符输入流,继承于Reader。BufferedReader的作用是为其他字符输入流添加一些缓冲功能。

2. BufferedReader函数列表

  • BufferedReader(Reader in)
  • BufferedReader(Reader in, int size)
  • void close()
  • void mark(int markLimit)
  • boolean markSupported()
  • int read()
  • int read(char[] buffer, int offset, int length)
  • String readLine()
  • boolean ready()
  • void reset()
  • long skip(long charCount)

3. BufferedReader源码分析

下面是BufferedReader的源码,基于JDK 1.7.0_07:

  • package java.io;
  • public class BufferedReader extends Reader {
  • private Reader in;
  • // 字符缓冲区
  • private char cb[];
  • // nChars是cb缓冲区中字符的总的个数
  • // nextChar是下一个要读取的字符在cb缓冲区中的位置
  • private int nChars, nextChar;
  • // 表示标记无效。它与UNMARKED的区别是:
  • // 1. UNMARKED 是压根就没有设置过标记
  • // 2. 而INVALIDATED是设置了标记,但是被标记位置太长,导致标记无效
  • private static final int INVALIDATED = -2;
  • // 表示没有设置标记
  • private static final int UNMARKED = -1;
  • // 标记
  • private int markedChar = UNMARKED;
  • // 标记能标记位置的最大长度
  • private int readAheadLimit = 0; /* Valid only when markedChar > 0 */
  • // skipLF(即skip Line Feed)是是否忽略换行符标记
  • private boolean skipLF = false;
  • // 设置标记时,保存的skipLF的值
  • private boolean markedSkipLF = false;
  • // 默认字符缓冲区大小
  • private static int defaultCharBufferSize = 8192;
  • // 默认每一行的字符个数
  • private static int defaultExpectedLineLength = 80;
  • // 创建Reader对应的BufferedReader对象,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;
  • }
  • // 创建Reader对应的BufferedReader对象,默认的BufferedReader缓冲区大小是8k
  • public BufferedReader(Reader in) {
  • this(in, defaultCharBufferSize);
  • }
  • // 确保BufferedReader是打开状态
  • private void ensureOpen() throws IOException {
  • if (in == null)
  • throw new IOException("Stream closed");
  • }
  • // 填充缓冲区函数,有以下两种情况被调用:
  • // 1. 缓冲区没有数据时,通过fill()可以向缓冲区填充数据
  • // 2. 缓冲区数据被读完,需更新时,通过fill()可以更新缓冲区的数据
  • private void fill() throws IOException {
  • // dst表示cb中填充数据的起始位置
  • int dst;
  • if (markedChar <= UNMARKED) {
  • // 没有标记的情况,则设dst = 0
  • dst = 0;
  • } else {
  • // delta表示当前标记的长度,它等于下一个被读取字符的位置减去标记的位置的差值;
  • int delta = nextChar - markedChar;
  • if (delta >= readAheadLimit) {
  • // 若当前标记的长度超过了标记上限(readAheadLimit),则丢弃标记
  • markedChar = INVALIDATED;
  • readAheadLimit = 0;
  • dst = 0;
  • } else {
  • if (readAheadLimit <= cb.length) {
  • // 若当前标记的长度没有超过了标记上限(readAheadLimit),
  • // 并且标记上限(readAheadLimit)小于或等于缓冲的长度;
  • // 则先将下一个要被读取的位置,距离我们标记的置符的距离间的字符保存到cb中
  • System.arraycopy(cb, markedChar, cb, 0, delta);
  • markedChar = 0;
  • dst = delta;
  • } else {
  • // 若当前标记的长度没有超过了标记上限(readAheadLimit),
  • // 并且标记上限(readAheadLimit)大于缓冲的长度;
  • // 则重新设置缓冲区大小,并将下一个要被读取的位置,距离我们标记的置符的距离间的字符保存到cb中
  • char ncb[] = new char[readAheadLimit];
  • System.arraycopy(cb, markedChar, ncb, 0, delta);
  • cb = ncb;
  • markedChar = 0;
  • dst = delta;
  • }
  • // 更新nextChar和nChars
  • nextChar = nChars = delta;
  • }
  • }
  • int n;
  • do {
  • // 从in中读取数据,并存储到字符数组cb中;
  • // 从cb的dst位置开始存储,读取的字符个数是cb.length - dst
  • // n是实际读取的字符个数;若n==0(即一个也没读到),则继续读取
  • n = in.read(cb, dst, cb.length - dst);
  • } while (n == 0);
  • // 如果从in中读到了数据,则设置nChars(cb中字符的数目)= dst+n,
  • // 并且nextChar(下一个被读取的字符的位置) = dst
  • if (n > 0) {
  • nChars = dst + n;
  • nextChar = dst;
  • }
  • }
  • // 从BufferedReader中读取一个字符,该字符以int的方式返回
  • public int read() throws IOException {
  • synchronized (lock) {
  • ensureOpen();
  • for (;;) {
  • // 若缓冲区的数据已经被读完,
  • // 则先通过fill()更新缓冲区数据
  • if (nextChar >= nChars) {
  • fill();
  • if (nextChar >= nChars)
  • return -1;
  • }
  • // 若要忽略换行符,则对下一个字符是否是换行符进行处理
  • if (skipLF) {
  • skipLF = false;
  • if (cb[nextChar] == '\n') {
  • nextChar++;
  • continue;
  • }
  • }
  • // 返回下一个字符
  • return cb[nextChar++];
  • }
  • }
  • }
  • // 将缓冲区中的数据写入到数组cbuf中,off是数组cbuf中的写入起始位置,len是写入长度
  • private int read1(char[] cbuf, int off, int len) throws IOException {
  • // 若缓冲区的数据已经被读完,则更新缓冲区数据
  • if (nextChar >= nChars) {
  • 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;
  • }
  • // 对read1()的封装,添加了同步处理和阻塞式读取等功能
  • 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;
  • }
  • }
  • // 读取一行数据,ignoreLF是是否忽略换行符
  • 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) { // 填充后还是在缓冲区尾表明流已读完
  • // 判断s中是否有数据,如果有就返回,否则返回null
  • 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 */
  • // 如果左边开头为'\n',则跳过(左开头的'\n'可能是上次调用readLine()留下的)
  • if (omitLF && (cb[nextChar] == '\n'))
  • nextChar++;
  • // 将是否忽略换行的标记全置为false,即接下来将不忽略换行
  • skipLF = false;
  • omitLF = false;
  • charLoop:
  • // 循环读取,直到遇到'\n'或'\r'
  • for (i = nextChar; i < nChars; i++) {
  • c = cb[i];
  • if ((c == '\n') || (c == '\r')) {
  • eol = true;
  • break charLoop;
  • }
  • }
  • // 记录读取字符的起始索引
  • startChar = nextChar;
  • // 更新nextChar为读取后的索引
  • nextChar = i;
  • if (eol) {
  • String str;
  • // 将cb缓冲区中,从startChar开始的i - startChar字符转换为字符串
  • if (s == null) {
  • // 如果s为空,则新创建一个字符串
  • str = new String(cb, startChar, i - startChar);
  • } else {
  • // 如果s不为空,则添加到s后面
  • s.append(cb, startChar, i - startChar);
  • str = s.toString();
  • }
  • // nextChar自增
  • nextChar++;
  • /**
  • * 如果c为'\r',则标记skipLF为true
  • * 这是因为当c为'\r'时,在下次读取时,nextChar即为'\n',需要跳过
  • */
  • if (c == '\r') {
  • skipLF = true;
  • }
  • return str;
  • }
  • /**
  • * 如果s为空,表明读到缓冲区尾都没有遇到换行符,
  • * 因此将整个缓冲区剩余未读的内容都拼接到s中,然后进行下一个循环,直至碰到换行符或读到流末尾为止
  • */
  • if (s == null)
  • s = new StringBuffer(defaultExpectedLineLength);
  • s.append(cb, startChar, i - startChar);
  • }
  • }
  • }
  • // 读取一行数据。不忽略换行符
  • public String readLine() throws IOException {
  • return readLine(false);
  • }
  • // 跳过n个字符
  • 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;
  • }
  • }
  • // 下一个字符是否可读
  • public boolean ready() throws IOException {
  • synchronized (lock) {
  • ensureOpen();
  • // 若忽略换行符为true;
  • // 则判断下一个符号是否是换行符,若是的话,则忽略
  • if (skipLF) {
  • if (nextChar >= nChars && in.ready()) {
  • fill();
  • }
  • if (nextChar < nChars) {
  • if (cb[nextChar] == '\n')
  • nextChar++;
  • skipLF = false;
  • }
  • }
  • return (nextChar < nChars) || in.ready();
  • }
  • }
  • // 始终返回true,因为BufferedReader支持mark(), reset()
  • public boolean markSupported() {
  • return true;
  • }
  • // 标记当前BufferedReader的下一个要读取位置。关于readAheadLimit的作用,参考后面的说明。
  • public void mark(int readAheadLimit) throws IOException {
  • if (readAheadLimit < 0) {
  • throw new IllegalArgumentException("Read-ahead limit < 0");
  • }
  • synchronized (lock) {
  • ensureOpen();
  • // 设置readAheadLimit
  • this.readAheadLimit = readAheadLimit;
  • // 保存下一个要读取的位置
  • markedChar = nextChar;
  • // 保存是否忽略换行符标记
  • markedSkipLF = skipLF;
  • }
  • }
  • // 重置BufferedReader的下一个要读取位置,
  • // 将其还原到mark()中所保存的位置。
  • 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;
  • }
  • }
  • public void close() throws IOException {
  • synchronized (lock) {
  • if (in == null)
  • return;
  • in.close();
  • in = null;
  • cb = null;
  • }
  • }
  • }

BufferReader的作用是为其它Reader提供缓冲功能。创建BufferReader时,通过它的构造函数可以指定某个Reader为参数。BufferReader会将该Reader中的数据分批读取,每次读取一部分到缓冲区中;操作完缓冲中的这部分数据之后,再从Reader中读取下一部分的数据。

下面就BufferReader中最重要的函数fill()进行详细解释:

  • // 填充缓冲区函数,有以下两种情况被调用:
  • // 1. 缓冲区没有数据时,通过fill()可以向缓冲区填充数据
  • // 2. 缓冲区数据被读完,需更新时,通过fill()可以更新缓冲区的数据
  • private void fill() throws IOException {
  • // dst表示cb中填充数据的起始位置
  • int dst;
  • if (markedChar <= UNMARKED) { // UNMARKED为-1
  • // 没有标记的情况,则设dst = 0
  • dst = 0;
  • } else {
  • // delta表示当前标记的长度,它等于下一个被读取字符的位置减去标记的位置的差值;
  • int delta = nextChar - markedChar;
  • if (delta >= readAheadLimit) {
  • // 若当前标记的长度超过了标记上限(readAheadLimit),则丢弃标记
  • markedChar = INVALIDATED;
  • readAheadLimit = 0;
  • dst = 0;
  • } else {
  • if (readAheadLimit <= cb.length) {
  • // 若当前标记的长度没有超过了标记上限(readAheadLimit),
  • // 并且标记上限(readAheadLimit)小于或等于缓冲的长度;
  • // 则先将下一个要被读取的位置,距离我们标记的置符的距离间的字符保存到cb中
  • System.arraycopy(cb, markedChar, cb, 0, delta);
  • markedChar = 0;
  • dst = delta;
  • } else {
  • // 若当前标记的长度没有超过了标记上限(readAheadLimit),
  • // 并且标记上限(readAheadLimit)大于缓冲的长度;
  • // 则重新设置缓冲区大小,并将下一个要被读取的位置,距离我们标记的置符的距离间的字符保存到cb中
  • char ncb[] = new char[readAheadLimit];
  • System.arraycopy(cb, markedChar, ncb, 0, delta);
  • cb = ncb;
  • markedChar = 0;
  • dst = delta;
  • }
  • // 更新nextChar和nChars
  • nextChar = nChars = delta;
  • }
  • }
  • int n;
  • do {
  • // 从in中读取数据,并存储到字符数组cb中;
  • // 从cb的dst位置开始存储,读取的字符个数是cb.length - dst
  • // n是实际读取的字符个数;若n==0(即一个也没读到),则继续读取
  • n = in.read(cb, dst, cb.length - dst);
  • } while (n == 0);
  • // 如果从in中读到了数据,则设置nChars(cb中字符的数目)= dst+n,
  • // 并且nextChar(下一个被读取的字符的位置) = dst
  • if (n > 0) {
  • nChars = dst + n;
  • nextChar = dst;
  • }
  • }

上面的fill()方法与BufferedInputStream的fill()方法的实现其实非常类似。这里将BufferedReader的fill()分为4种不同的情况,下面将一一介绍。

  1. 无mark标记的情况,即markedChar <= UNMARKEDcb中的数据已经被读取完。

这种情况最常见的,需要丢弃cb中现有的所有数据(即直接将dst置为0),然后从原始流读取数据填充整个cb。相应的简化代码如下:

  • private void fill() throws IOException {
  • // dst表示cb中填充数据的起始位置
  • int dst;
  • // 没有标记的情况,则设dst = 0
  • dst = 0;
  • int n;
  • do {
  • // 从in中读取数据,并存储到字符数组cb中;
  • // 从cb的dst位置开始存储,读取的字符个数是cb.length - dst
  • // n是实际读取的字符个数;若n==0(即一个也没读到),则继续读取
  • n = in.read(cb, dst, cb.length - dst);
  • } while (n == 0);
  • // 如果从in中读到了数据,则设置nChars(cb中字符的数目)= dst+n,
  • // 并且nextChar(下一个被读取的字符的位置) = dst
  • if (n > 0) {
  • nChars = dst + n;
  • nextChar = dst;
  • }
  • }

对应的cb示意图如下:

1.fill()第一种情况.png

  1. 有mark标记的情况,且markedChar > UNMARKED,即markedChar >= 0;且从markedCharnextChar的数据长度大于等于mark标记上限readAheadLimitcb中的数据已经被读取完。

此时将丢弃markedChar标记(将其置为INVALIDATED(-2)),并将readAheadLimit置为0,dst置为0,然后丢弃cb中的数据并从原始流读取数据填充整个cb。相应的简化代码如下:

  • private void fill() throws IOException {
  • // dst表示cb中填充数据的起始位置
  • int dst;
  • // 若当前标记的长度超过了标记上限(readAheadLimit),则丢弃标记
  • markedChar = INVALIDATED;
  • readAheadLimit = 0;
  • dst = 0;
  • int n;
  • do {
  • // 从in中读取数据,并存储到字符数组cb中;
  • // 从cb的dst位置开始存储,读取的字符个数是cb.length - dst
  • // n是实际读取的字符个数;若n==0(即一个也没读到),则继续读取
  • n = in.read(cb, dst, cb.length - dst);
  • } while (n == 0);
  • // 如果从in中读到了数据,则设置nChars(cb中字符的数目)= dst+n,
  • // 并且nextChar(下一个被读取的字符的位置) = dst
  • if (n > 0) {
  • nChars = dst + n;
  • nextChar = dst;
  • }
  • }

对应的cb示意图如下:

2.fill()第二种情况.png

  1. 有mark标记的情况,且markedChar > UNMARKED,即markedChar >= 0;且从markedCharnextChar的数据长度小于mark标记上限readAheadLimit,且readAheadLimit的值小于cb的总大小,cb中的数据已经被读取完。

此时将cb中从markedChar开始到readAheadLimit的数据(长度为delta)往cb前移,移到从0开始的位置,并将markedChar置为0,这个操作是为了保证从markedChar开始的数据还能被重复读取,然后从原始流读取数据填充cb剩余的空闲空间(即从delta开始的空间),且更新dstnextChardeltanCharscb中现有的数据。相应的简化代码如下:

  • private void fill() throws IOException {
  • // dst表示cb中填充数据的起始位置
  • int dst;
  • // delta表示当前标记的长度,它等于下一个被读取字符的位置减去标记的位置的差值;
  • int delta = nextChar - markedChar;
  • // 若当前标记的长度没有超过了标记上限(readAheadLimit),
  • // 并且标记上限(readAheadLimit)小于或等于缓冲的长度;
  • // 则先将下一个要被读取的位置,距离我们标记的置符的距离间的字符保存到cb中
  • System.arraycopy(cb, markedChar, cb, 0, delta);
  • markedChar = 0;
  • dst = delta;
  • // 更新nextChar和nChars
  • nextChar = nChars = delta;
  • int n;
  • do {
  • // 从in中读取数据,并存储到字符数组cb中;
  • // 从cb的dst位置开始存储,读取的字符个数是cb.length - dst
  • // n是实际读取的字符个数;若n==0(即一个也没读到),则继续读取
  • n = in.read(cb, dst, cb.length - dst);
  • } while (n == 0);
  • // 如果从in中读到了数据,则设置nChars(cb中字符的数目)= dst+n,
  • // 并且nextChar(下一个被读取的字符的位置) = dst
  • if (n > 0) {
  • nChars = dst + n;
  • nextChar = dst;
  • }
  • }

对应的cb示意图如下:

3.fill()第三种情况.png

  1. 有mark标记的情况,且markedChar > UNMARKED,即markedChar >= 0;且从markedCharnextChar的数据长度小于mark标记上限readAheadLimit,且readAheadLimit的值大于cb的总大小,cb中的数据已经被读取完。

此时将给cb扩容,即新创建一个总容量为当前readAheadLimit值的缓冲区,然后将旧缓冲区中从markedChar开始的delta长度的数据复制到新缓冲区中从0开始的位置,并且使用将新缓冲区赋值给cb引用,然后从原始流读取数据填充剩余的空闲空间。相应的简化代码如下:

  • private void fill() throws IOException {
  • // dst表示cb中填充数据的起始位置
  • int dst;
  • // delta表示当前标记的长度,它等于下一个被读取字符的位置减去标记的位置的差值;
  • int delta = nextChar - markedChar;
  • // 若当前标记的长度没有超过了标记上限(readAheadLimit),
  • // 并且标记上限(readAheadLimit)大于缓冲的长度;
  • // 则重新设置缓冲区大小,并将下一个要被读取的位置,距离我们标记的置符的距离间的字符保存到cb中
  • char ncb[] = new char[readAheadLimit];
  • System.arraycopy(cb, markedChar, ncb, 0, delta);
  • cb = ncb;
  • markedChar = 0;
  • dst = delta;
  • // 更新nextChar和nChars
  • nextChar = nChars = delta;
  • int n;
  • do {
  • // 从in中读取数据,并存储到字符数组cb中;
  • // 从cb的dst位置开始存储,读取的字符个数是cb.length - dst
  • // n是实际读取的字符个数;若n==0(即一个也没读到),则继续读取
  • n = in.read(cb, dst, cb.length - dst);
  • } while (n == 0);
  • // 如果从in中读到了数据,则设置nChars(cb中字符的数目)= dst+n,
  • // 并且nextChar(下一个被读取的字符的位置) = dst
  • if (n > 0) {
  • nChars = dst + n;
  • nextChar = dst;
  • }
  • }

对应的cb示意图如下:

4.fill()第四种情况.png

4. BufferedReader示例

我们这里以之前在BufferedWriter中创建的文件file.data作为演示的数据来源,file.data内容如下:

  • 1. abcdefg
  • 2. hijklmn
  • 3. opq rst
  • 4. uvw xyz
  • 5. ABCDEFG
  • 6. HIJKLMN
  • 7. OPQ RST
  • 8. UVW XYZ

BufferedWriter示例代码如下:

  • package com.coderap;
  • import java.io.BufferedReader;
  • import java.io.FileReader;
  • import java.io.IOException;
  • public class BufferedReaderTest {
  • public static void main(String[] args) throws IOException {
  • BufferedReader bufferedReader = new BufferedReader(new FileReader("file.data"));
  • // 读取一个字符
  • System.out.println("read: " + String.valueOf((char)bufferedReader.read()));
  • // 读取字符到长度为4的字符数组中
  • char[] chars = new char[4];
  • bufferedReader.read(chars);
  • System.out.println("read: " + new String(chars));
  • // mark标记
  • bufferedReader.mark(1024);
  • // reset循环重复读取
  • for (int i = 0; i < 3; i++) {
  • bufferedReader.read(chars);
  • System.out.println("read: " + new String(chars));
  • bufferedReader.reset();
  • }
  • // 读取剩余的行
  • String line;
  • while ((line = bufferedReader.readLine()) != null) {
  • System.out.println("readLine: " + line);
  • }
  • bufferedReader.close();
  • }
  • }

运行结果如下:

  • read: 1
  • read: . ab
  • read: cdef
  • read: cdef
  • read: cdef
  • readLine: cdefg
  • readLine: 2. hijklmn
  • readLine: 3. opq rst
  • readLine: 4. uvw xyz
  • readLine: 5. ABCDEFG
  • readLine: 6. HIJKLMN
  • readLine: 7. OPQ RST
  • readLine: 8. UVW XYZ