(最近刚来到简书平台,以前在CSDN上写的一些东西,也在逐渐的移到这儿来,有些篇幅是很早的时候写下的,因此可能会看到一些内容杂乱的文章,对此深感抱歉,以下为正文)
正文
本篇讲述的是Java IO包中的BufferedReader和BufferedWriter。从名字中可以看出它们分别是Reader和Writer的子类,它们的特点是在对流进行读写操作时,内置了缓存区,通过减少与磁盘之间IO操作的次数,从而提升了读写效率,下面我们来简要的看看它们的源码。
BufferedReader.java
package java.io;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class BufferedReader extends Reader {
//内置了一个Reader对象句柄in,用于接收传入的Reader对象。
private Reader in;
//定义了一个char类型的数组,作为内置的数据存储区,默认大小为8k。
private char cb[];
//声明了两个int型变量nChars和nextChar,nChars表示缓存区中存在的字符数据的个数,nextChar表示下一个要读取的字符内容在缓存区中所在的位置。
private int nChars, nextChar;
//定义了两个常量值分别代表了流内标记的两种状态,INVALIDATED表示流中曾经有过标记,但是超过了标记的限制长度,标记失效,UNMARKED则是没有做过标记。
private static final int INVALIDATED = -2;
private static final int UNMARKED = -1;
//声明了一个int型变量markedChar,该变量表示了当前流中的标记状态,初始化默认值为UNMARKED。
private int markedChar = UNMARKED;
//声明了一个int型变量readAheadLimit,该值是流中标记的限制长度,如果标记位置超过了该值,则标记会被抛弃。该值只有大于零时,才起作用。
private int readAheadLimit = 0;
//声明了一个boolean型变量skipLF,该值用于表示是否忽略换行标记,初始化默认值为false,表示不忽略。
private boolean skipLF = false;
//声明了一个boolean型变量markedSkipLF,该值用于保存skipLF的状态。
private boolean markedSkipLF = false;
//定义了一个int型的变量值defaultCharBufferSize,该值表示内置缓存区中的容量大小,初始化默认值为8k。
private static int defaultCharBufferSize = 8192;
//定义了一个int型的变量值defaultExpectedLineLength,该值表示了每行中字符数的长度,初始化默认值为80。
private static int defaultExpectedLineLength = 80;
/**
* 定义了带两个参数的构造函数,第一个参数为一个Reader型对象,用于给内部定义的Reader对象句柄赋值,第二个参数为一个int型参数sz,用来初始化内部缓存区的
* 容量。
*/
public BufferedReader(Reader in, int sz) {
//调用父类Reader中的构造方法,将传入的Reader对象作为锁对象,为后面的同步操作提供同步锁。
super(in);
//对传入的参数进行安全检测,如果sz小于0则抛出对应异常。
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
//将传入的Reader对象,赋值给内部声明的Reader对象句柄。
this.in = in;
//初始化内置的缓存数组cb,通过传入的参数sz来确定cb的容量大小。
cb = new char[sz];
初始化nextChar,nChars的值,刚开始都为0。
nextChar = nChars = 0;
}
/**
* 定义了一个带一个参数的构造函数,传入的参数类型为一个Reader对象,内部实际是调用上面的构造方法,内置缓存区使用默认大小创建,容量为8k。
*/
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
/**
* 该方法用于检测当前流是否关闭。如果已经关闭,则抛出相应的异常。
*/
private void ensureOpen() throws IOException {
if (in == null)
throw new IOException("Stream closed");
}
/**
* 该方法是整个类中的核心方法,用于向内置的缓存区中填充数据。
*/
private void fill() throws IOException {
//定义了一个int型值dst,用来表示内置缓存区中有效数据的起始位置。
int dst;
//如果markedChar <= UNMARKED,则表示当前流中没有标记,因此可以大胆地直接将dst重置为0,后面会对缓存区进行重新填充。
if (markedChar <= UNMARKED) {
dst = 0;
} else {
//以下情况表示流中存在标记情况。//定义了一个int型值delta,用于接收即将读取的下一个字符位置于流中标记位置的差值。
int delta = nextChar - markedChar;
//如果delta>=readAheadLimit,表示当前标记的位置已经超过了标记长度的限制,那么此时直接将标记清空。
if (delta >= readAheadLimit) {
//将流标记状态恢复为INVALIDATED,表示因为超过标记长度限制,流标记失效。
markedChar = INVALIDATED;
//因为流标记失效,所以此时流标记的长度限制也归零。
readAheadLimit = 0;
//将内部缓存区的有效数据的起始位置归零。
dst = 0;
} else {
//如果标记的限制长度readAheadLimit小于等于内置缓存区的容量,那么此时将内置缓存区中从标记处的数据复制到缓存数组的开头,同时将标记处置为0,
//此时缓存区中有效数据长度为delta。
if (readAheadLimit <= cb.length) {
System.arraycopy(cb, markedChar, cb, 0, delta);
markedChar = 0;
dst = delta;
} else {
//如果标记的限制长度readAheadLimit大于内置缓存区的容量,那么直接扩容缓存区数组,将其容量直接扩展到标记的限制值大小,将原先的内置缓存
//区中自标记处开始的内容复制到扩容的新数组处,并将扩容后的数组赋值给内置的缓存数组cb。此时标记处重置为0,缓存区中有效长度为delta。
char ncb[] = new char[readAheadLimit];
System.arraycopy(cb, markedChar, ncb, 0, delta);
cb = ncb;
markedChar = 0;
dst = delta;
}
//此时即将读取的下一个字符的索引和数组缓存区中的有效数字都等于delta的值。
nextChar = nChars = delta;
}
}
//通过一个循环,向内置的缓存区中填充数据。为nChars,nextChar更新状态。
int n;
do {
n = in.read(cb, dst, cb.length - dst);
} while (n == 0);
if (n > 0) {
nChars = dst + n;
nextChar = dst;
}
}
/**
* 定义了一个read方法,每次读取一个字符。事实上是对read1的一个封装,添加了同步和阻塞等功能。
*/
public int read() throws IOException {
synchronized (lock) {
//调用ensureOpen方法,确认当前流是否关闭。
ensureOpen();
for (;;) {
//如果nextChar>=nChars,表示当前缓存区中的数据已读完,此时需要调用fill方法重新向缓存区中填充数据。
if (nextChar >= nChars) {
fill();
//如果缓存区中更新后,nextChar还是大于等于nChars,那么表示此时文件已读到末尾,返回-1。
if (nextChar >= nChars)
return -1;
}
//根据skipLF的值,判断是否对换行符进行处理,如果为true,则跳过换行符。
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '\n') {
nextChar++;
continue;
}
}
return cb[nextChar++];
}
}
}
/**
* 定义了一个带三个参数的read方法,一次可以读取多个字符数据。
*/
private int read1(char[] cbuf, int off, int len) throws IOException {
//如果nextChar>=nChars,则表示当前缓存区中的内容已经全部读完。
if (nextChar >= nChars) {
//如果需要读取的长度大于等于内置缓存区的容量,并且流中没有标记并且对换行符不做处理的时候,那么直接调用传入的Reader对象相应的read方法,直接从
//原始数据流中读取指定长度的数据,避免了先拷贝到缓存区再从缓存区中取出的麻烦。
if (len >= cb.length && markedChar <= UNMARKED && !skipLF) {
return in.read(cbuf, off, len);
}
//向内置缓存区中填充数据。
fill();
}
//如果更新完内置缓存区后,nextChar仍然大于等于nChars,则表示文件已经读取完毕,此时返回-1。
if (nextChar >= nChars) return -1;
//根据skipLF的值,判断是否对换行符进行处理,如果为true,则进行相应处理。
if (skipLF) {
skipLF = false;
//如果下一个读取的字符为换行符,那么跳过,并检测下一次读取是否超过缓存区容量,如果超过则进行缓存区填充,填充完毕后再次进行检测,如果nextChar
//仍然大于等于nChars,那么表示文件内容已经读完,则返回-1。
if (cb[nextChar] == '\n') {
nextChar++;
if (nextChar >= nChars)
fill();
if (nextChar >= nChars)
return -1;
}
}
//定义了一个int型变量n用来存放len,和nChars-nextChar之间的最小值。该值代表着实际读取的字符数量。
int n = Math.min(len, nChars - nextChar);
//通过System.arraycopy方法,从内置的数据缓存区中向传入的字符数组拷贝指定长度的数据。
System.arraycopy(cb, nextChar, cbuf, off, n);
//nextChat加上实际读取的字符数量,最终返回实际读取的字符数量。
nextChar += n;
return n;
}
/**
* 定义了带一个参数的readLine方法,该方法一次读取一行数据,传入的参数是一个boolean型数值,表示是否忽略换行符,该方法最终返回读取到的字符串。
*/
String readLine(boolean ignoreLF) throws IOException {
//声明了一个StringBuffer对象,用于存储读取到的字符数据。
StringBuffer s = null;
//声明了一个int型变量startChar,表示数据读取的起始位置。
int startChar;
synchronized (lock) {
//检测流是否关闭
ensureOpen();
//定义了一个boolean型变量omitLF,用于判断是否负略换行符,通过对ignoreLF和skipLF两个值进行或操作来得出是否需要忽略换行符。
boolean omitLF = ignoreLF || skipLF;
//定义了一个循环来读取数据,在循环外部定义了一个标签bufferLoop,方便跳出循环。
bufferLoop:
for (;;) {
//如果nextChar >= nChars,则表示读取的位置超过了数组缓存区中容量,那么此时需要向内置缓存区中重新填充数据。
if (nextChar >= nChars)
fill();
//数据填充结束后再次对读取位置进行检测,如果nextChar仍然大于等于nChars,那么表示文件已经读取完毕。
if (nextChar >= nChars) {
//如果次苏沪杭存储内容的s中有内容,则将s转化为字符串并返回,否则返回null。
if (s != null && s.length() > 0)
return s.toString();
else
return null;
}
//定义了一个boolean型变量eol(end of line),该变量用于表示是否是以换行符结尾。
boolean eol = false;
char c = 0;
int i;
//如果遇到了换行符那么跳过该字符,然后重置skipLF,omitLF状态。
if (omitLF && (cb[nextChar] == '\n'))
nextChar++;
skipLF = false;
omitLF = false;
//在循环中嵌套了一个循环用于寻找换行符,并添加了一个标记charLoop,方便跳出循环。
charLoop:
for (i = nextChar; i < nChars; i++) {
c = cb[i];
if ((c == '\n') || (c == '\r')) {
eol = true;
break charLoop;
}
}
startChar = nextChar;
nextChar = i;
//如果eol为true,即检测得到换行符,那么此时将缓存区中的数据装换成String类型并返回
if (eol) {
String str;
//如果s为null,那么通过缓存区内容新建一个String对象传给str,否则调用append方法在s后追加内容,然后通过toString方法返回String类型数据
//给str。
if (s == null) {
str = new String(cb, startChar, i - startChar);
} else {
s.append(cb, startChar, i - startChar);
str = s.toString();
}
//读取的位置向后移位,如果此时c为'\r',那么skipLF置为true。
nextChar++;
if (c == '\r') {
skipLF = true;
}
return str;
}
if (s == null)
s = new StringBuffer(defaultExpectedLineLength);
s.append(cb, startChar, i - startChar);
}
}
}
/**
* 定义了一个readLine方法,每次读取一行数据。本质就是调用上面带参的readLine方法,忽略换行符。
*/
public String readLine() throws IOException {
return readLine(false);
}
/**
* 定义了一个带参的skip方法,用于跳过指定参数个数的字符。传入的参数是一个long型数据。
*/
public long skip(long n) throws IOException {
//对传入的参数进行安全监测,如果n小于零,则抛出相应异常。
if (n < 0L) {
throw new IllegalArgumentException("skip value is negative");
}
synchronized (lock) {
//检测流是否关闭。
ensureOpen();
long r = n;
while (r > 0) {
//如果当前读取位置超过缓存区容量,那么调用fill方法,向缓存中重新填充数据。
if (nextChar >= nChars)
fill();
//缓存区刷新后,如果读取位置仍然大于等于缓存区中容量,那么此时跳出循环。
if (nextChar >= nChars)
break;
//通过skipLF来判读是否需要对换行符进行处理。
if (skipLF) {
//如果需要跳过换行符,那么当遇到'\n'时,直接将读取索引向后移位
skipLF = false;
if (cb[nextChar] == '\n') {
nextChar++;
}
}
//定义了一个long型变量d,用于存放缓存区中剩余的字符数量。
long d = nChars - nextChar;
//如果需要跳过的字符数量小于等于d,那么直接将读取索引向后移动r即可,然后将r置为0表示全部跳过。否则将r减去d,nextChar移动到缓存区尾部,
//表示将缓存区中的内容都跳过。
if (r <= d) {
nextChar += r;
r = 0;
break;
}
else {
r -= d;
nextChar = nChars;
}
}
//最终返回n-r,表示实际跳过的字符数。
return n - r;
}
}
/**
* 定义了一个ready方法,用于判断流中数据是否可读。
*/
public boolean ready() throws IOException {
synchronized (lock) {
//检测当前流是否关闭
ensureOpen();
//通过skipLF来判断是否需要对换行符进行处理。
if (skipLF) {
//如果当前读取位置超过了缓存区容量,且传入的Reader对象是可读的,那么调用fill方法,向缓存中填充数据。
if (nextChar >= nChars && in.ready()) {
fill();
}
//缓存区刷新后,如果读取位置在缓存区内容范围内,当遇到换行符'\n'时,将读取位置向后移动一位,最后重置skipLF状态。
if (nextChar < nChars) {
if (cb[nextChar] == '\n')
nextChar++;
skipLF = false;
}
}
//最终通过当前读取位置是否在缓存区内容范围内和传入的Reader对象in是否可读来决定当前流是否可读。
return (nextChar < nChars) || in.ready();
}
}
/**
* 定一了一个markSupported方法,通过其返回值来判断当前流是否支持标记功能,此处总是返回true,表示支持流标记功能。
*/
public boolean markSupported() {
return true;
}
/**
* 定义了一个带参的mark方法,用于在流中当前位置留下标记,通过与reset方法联合使用,可以在读取过程中返回到标记位置。传入的参数为一个int型值,该值用于
* 限定标记允许的最大长度。
*/
public void mark(int readAheadLimit) throws IOException {
//对传入的参数进行安全监测,如果readAheadLimit小于零,那么抛出相应的异常。
if (readAheadLimit < 0) {
throw new IllegalArgumentException("Read-ahead limit < 0");
}
synchronized (lock) {
//监测当前流是否关闭。
ensureOpen();
//将最初声明的readAheadLimit赋值。
this.readAheadLimit = readAheadLimit;
//记录下下一个要读取的字符的位置。
markedChar = nextChar;
//记录下是否需要忽略换行符。
markedSkipLF = skipLF;
}
}
/**
* 定义了一个reset方法,通过与mark方法联合使用,可以在读取数据时返回到标记处重新进行数据读取。
*/
public void reset() throws IOException {
synchronized (lock) {
//检测当前流是否关闭。
ensureOpen();
//检测markdChar,如果其小于0,表示当前流中标记功能无用。
if (markedChar < 0)
throw new IOException((markedChar == INVALIDATED)
? "Mark invalid"
: "Stream not marked");
//将下一个读取位置置为标记处的位置。将skipLF状态置为保存的状态。
nextChar = markedChar;
skipLF = markedSkipLF;
}
}
/*
* 定义了一个close方法用于关闭流,并将流中缓存区清除。
*/
public void close() throws IOException {
synchronized (lock) {
//如果in已经为null,则返回,否则调用其close方法,并将内置的缓存区数组置为null。
if (in == null)
return;
try {
in.close();
} finally {
in = null;
cb = null;
}
}
}
/**
* 该方法是java1.8中的新特性,可以将I/O流转换为字符串流方便我们对其进行操作。
*/
public Stream<String> lines() {
Iterator<String> iter = new Iterator<String>() {
String nextLine = null;
@Override
public boolean hasNext() {
if (nextLine != null) {
return true;
} else {
try {
nextLine = readLine();
return (nextLine != null);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
@Override
public String next() {
if (nextLine != null || hasNext()) {
String line = nextLine;
nextLine = null;
return line;
} else {
throw new NoSuchElementException();
}
}
};
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
iter, Spliterator.ORDERED | Spliterator.NONNULL), false);
}
}
BufferedWriter.java
package java.io;
public class BufferedWriter extends Writer {
//内置了一个Writer句柄,用于接收传入的Writer对象,用于负责真正的磁盘之间的IO操作。
private Writer out;
//内置了一个char型数组,用于当做存储缓存区。
private char cb[];
//声明了两个int型变量,nChars表示内置缓存区数组的容量大小,nextChar表示下一个写入的字符的位置下标。
private int nChars, nextChar;
//声明了一个静态的int型变量defaultCharBufferSize,赋值8k,是内置缓存区的默认容量大小。
private static int defaultCharBufferSize = 8192;
//声明了一个String类型的变量,用于接收运行环境中的换行符(不同环境中的换行符不同)。
private String lineSeparator;
/**
* 一个带一个参数的构造方法,传入的参数为一个Writer型对象。内部实质是调用下面带两个参数的构造方法。初始化时默认缓存区大小为8k。
*/
public BufferedWriter(Writer out) {
this(out, defaultCharBufferSize);
}
/**
* 一个带两个参数的构造方法,第一个参数为一个Writer类型对象,将其赋值给开始声明的Writer对象句柄,第二个参数是一个int型数值,它决定着初始化时的内置缓
* 区的容量大小。
*/
public BufferedWriter(Writer out, int sz) {
//调用父类的构造函数,将writer对象作为之后进行同步操作的锁对象。
super(out);
//如果传入的int型变量<0,则抛出非法数据的异常。
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
//将传入的Wrter对象赋值给之前声明的Writer对象句柄out,根据传入的int型变量sz初始化内置的缓存区数组cb,为nChars、nextChar赋上初始值。
this.out = out;
cb = new char[sz];
nChars = sz;
nextChar = 0;
//根据当前运行环境,获得换行符并赋值给最初定义的String型变量lineSeparator。
lineSeparator = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("line.separator"));
}
/**
* 定义了一个ensureOpen方法,见名知其义,用来判断当前流是否关闭,如果关闭了则抛出IO异常。
*/
private void ensureOpen() throws IOException {
if (out == null)
throw new IOException("Stream closed");
}
/**
* 定义了一个flushBuffer方法,用于将内置缓存区的数据写入到内置的Writer里。
*/
void flushBuffer() throws IOException {
synchronized (lock) {
//检测Writer流是否关闭
ensureOpen();
//如果读取位置为0,表示缓存区中没有数据,无需清空,所以直接return。
if (nextChar == 0)
return;
//如果缓存区中有数据,则将缓存区中数据写出,将nextChar标记位重置为0。
out.write(cb, 0, nextChar);
nextChar = 0;
}
}
/**
* 定义了一个带一个参数的write方法,一次写入一个字符。
*/
public void write(int c) throws IOException {
synchronized (lock) {
//检测流是否关闭
ensureOpen();
//如果内置的缓存数组容量不足,调用flushBuffer方法,将缓存内容写出并清空。
if (nextChar >= nChars)
flushBuffer();
//向缓存中写入指定字符。
cb[nextChar++] = (char) c;
}
}
/**
* 定义了一个带两个参数的min方法,用于返回两个值之间的最小值。这里没有使用java.lang.Math中的对应方法,也许提升了运行效率吧,毕竟少加载了一个类。
*/
private int min(int a, int b) {
if (a < b) return a;
return b;
}
/**
* 定义了一个带三个参数的write方法,一次可以写入多个字符。第一个字符为一个char型数组,用于存放需要写入的数据,第二个参数为一个int型数值,表示数组写入
* 的起始位置,第三个int型参数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;
}
//如果需要写入的数据的长度大于了内置缓存区的容量,那么先调用flushBuffer方法将缓存区中的数据先写入,然后直接使用Writer写入数据,不使用缓存区,
//这样就避免了先向缓存区中填充数据再从缓存区写入这样麻烦的步骤。
if (len >= nChars) {
flushBuffer();
out.write(cbuf, off, len);
return;
}
//定义了一个int型变量b表示当前位置,一个int型变量t表示当前位置写入指定len长度后的位置。
int b = off, t = off + len;
//使用了一个循环,用于写入指定长度的数据。
while (b < t) {
//定义了一个int型变量d,比较缓存区数组剩余的容量和需要写入的长度,将数值小的一方赋值给d。
int d = min(nChars - nextChar, t - b);
//将传入cbuf中d长度的内容填充到缓存区中。
System.arraycopy(cbuf, b, cb, nextChar, d);
//更新当前位置
b += d;
nextChar += d;
//如果当前位置超过了内置缓存容量,调用flushBuffer方法,将缓存内容flush值Writer。
if (nextChar >= nChars)
flushBuffer();
}
}
}
/**
* 该方法用于直接写入字符串形式的数据。
*/
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();
}
}
}
/**
* 该方法用于向流中写入一个换行符,起到换行的作用。
*/
public void newLine() throws IOException {
write(lineSeparator);
}
/**
* 定义了一个flush方法,首先调用flushBuffer方法,将内置的缓存flush到Writer中,然后调用Wrter的flush方法,将数据实际写入至目的地。
*/
public void flush() throws IOException {
synchronized (lock) {
flushBuffer();
out.flush();
}
}
/**
* 定义了一个close方法,用于关闭流。
*/
@SuppressWarnings("try")
public void close() throws IOException {
synchronized (lock) {
//如果已经关闭则无需其它操作
if (out == null) {
return;
}
//否则关闭流并清空缓存区,最后将Writer对象句柄和缓存数组都指向null,让jvm进行资源回收。
try (Writer w = out) {
flushBuffer();
} finally {
out = null;
cb = null;
}
}
}
}
通过上面简单的分析我们队BufferedReader和BufferedWriter有了初步的认识,下面通过一个简单的例子来说明这两个类的基本用法。由于笔者使用的不是jdk8,所以这里的新特性就没有展示了。
package BufferedIOTest;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedIOTest2 {
public static void main(String[] args) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(new File(
"./src/file/test.txt")));
BufferedWriter bw = new BufferedWriter(new FileWriter(new File(
"./src/file/testcopy2.txt")))) {
String len;
while ((len = br.readLine()) != null) {
bw.write(len+(br.ready()?"\n":""));
}
System.out.println("copying file has been finished..");
}
}
}
执行上述代码,可以将制定位置的文件进行拷贝。值得注意的是上面的例子在读取时使用了BufferedReader的readLine方法,一次读取一行数据,在写入数据的时候需要自己添加对应的换行符。
以上为本篇的全部内容。