今天学习BufferedReader与BufferedWriter。
BufferedReader,字符缓冲输入流,作用是为其他输入流提供缓冲功能。BufferedReader从其他字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
通常,Reader所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。因此,建议用BufferedReader包装所有其read()操作可能开销很高的Reader(如FileReader和InputStreamReader)。例如,
BufferedReader in = new BufferedReader(new FileReader("foo.in"));
将缓冲指定文件的输入。如果没有缓冲,则每次调用read()或readLine()都会导致从文件中读取字节,并将其转换为字符后返回,而这是极其低效的。
BufferedWriter,字符缓冲输出流,作用是为其他输出流提供缓冲功能。BufferedWriter将文本写入其他字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。
通常Writer将其输出立即发送到底层字符或字节流。除非要求提示输出,否则建议用BufferedWriter包装所有其write()操作可能开销很高的 Writer(如FileWriters和OutputStreamWriters)。例如,PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));
将缓冲PrintWriter对文件的输出。如果没有缓冲,则每次调用print()方法会导致将字符转换为字节,然后立即写入到文件,而这是极其低效的。
为了更好地理解BufferedReader的源码,我们先来了解其思想。BufferedReader是如何为其他输入流提供缓冲功能的?提供缓冲为什么能实现字符、数组和行的高效读取?
先来看看第一次问题,BufferedReader是如何为其他输入流提供缓冲功能的?
创建BufferReader时,我们会通过它的构造函数指定某个Reader为参数。BufferedReader将Reader中的数据分批取到自己的buffer中(内置缓存字符数组),然后处理buffer中的数据。操作完buffer中的数据后,BufferedReader会从Reader中读取下一批数据到buffer中供程序处理。如此循环往复,直到Reader中数据被读取完毕。这其中就有个关键的方法fill(),每当buffer中数据被读取完之后,fill()就会从Reader中将数据填充到buffer中。这个方法下面会仔细讲解。
提供缓冲为什么能实现字符、数组和行的高效读取?
一是提高了读取的效率、二是减少了打开存储介质的连接次数。缓冲中的数据实际上是保存在内存中,而原始数据可能是保存在硬盘中。从内存中读取数据的速度比从硬盘读取数据的速度至少快10倍以上。
到这里又有个问题,为什么不一次性将Reader中全部数据都读取到缓冲中呢?
第一,读取全部的数据所需要的时间可能会很长。第二,内存价格很贵,容量远没有硬盘那么大。我们能做的就是在效率和成本之间找到平衡点。大多数情况下,默认值就足够大了。
下面来学习下BufferedReader的源码,看看BufferedReader是如何为其他输入流提供缓冲功能,实现字符、数组和行的高效读取的。
public class BufferedReader extends Reader {
// 底层字符输入流
private Reader in;
// 字符缓冲区
private char cb[];
// nChars是cb字符缓冲区中字符的总的个数
// nextChar是下一个要读取的字符在cb缓冲区中的位置
private int nChars, nextChar;
// 表示标记无效。设置了标记,但是被标记位置由于某种原因导致标记无效
private static final int INVALIDATED = -2;
//表示没有标记
private static final int UNMARKED = -1;
//标记位置初始化为UNMARKED
private int markedChar = UNMARKED;
//在仍保留该标记的情况下,对可读取字符数量的限制。
//在读取达到或超过此限制的字符后,尝试重置流可能会失败。
//限制值大于输入缓冲区的大小将导致分配一个新缓冲区,其大小不小于该限制值。因此应该小心使用较大的值。
private int readAheadLimit = 0; /* Valid only when markedChar > 0 */
//表示是否跳过换行符。(skipLF ,skip line feed)
private boolean skipLF = false;
//表示当做了标记时,是否忽略换行符。
private boolean markedSkipLF = false;
//字符缓冲区默认大小
private static int defaultCharBufferSize = 8192;
//每行默认的字符个数
private static int defaultExpectedLineLength = 80;
/**
* 创建指定底层字符输入流in和指定字符缓冲区大小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;
}
/**
* 创建指定底层字符输入流in和默认字符缓冲区大小defaultCharBufferSize的BufferedReader
*/
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
/**
* 检查BufferedReader是否处于open状态。
* 如果in不为null,BufferedReader即为open状态。
*/
private void ensureOpen() throws IOException {
if (in == null)
throw new IOException("Stream closed");
}
/**
* 填充缓冲区。
* 如果标记有效,要考虑标记。
*/
private void fill() throws IOException {
//cb中填充数据的起始位置
int dst;
//如果没有标记,从缓冲区索引为0的位置开始填充
if (markedChar <= UNMARKED) {
dst = 0;
} else {//如果有标记
//delta为标记位置与下个读取字符之间的距离
int delta = nextChar - markedChar;
//如果delta超出readAheadLimit,标记即为无效。
if (delta >= readAheadLimit) {
markedChar = INVALIDATED;
readAheadLimit = 0;
dst = 0;
} else {
//如果delta没有超出readAheadLimit,即标记有效
//且readAheadLimit小于等于缓冲区长度
//将markedChar与nextChar之间的字符写入到缓冲区中
if (readAheadLimit <= cb.length) {
/* Shuffle in the current buffer */
System.arraycopy(cb, markedChar, cb, 0, delta);
markedChar = 0;
dst = delta;
} else {
//如果delta没有超出readAheadLimit,即标记有效
//且readAheadLimit大于缓冲区长度
//将重新设置缓冲区大小,markedChar与nextChar之间的字符写入到缓冲区中
char ncb[] = new char[readAheadLimit];
System.arraycopy(cb, markedChar, ncb, 0, delta);
cb = ncb;
markedChar = 0;
dst = delta;
}
nextChar = nChars = delta;
}
}
int n;
//从底层输入流中读取数据,并存储到缓冲区b中
//如果没有读取到数据,就继续读,直到读到数据或者到达流末尾为止
do {
n = in.read(cb, dst, cb.length - dst);
} while (n == 0);
//如果读到了数据
if (n > 0) {
nChars = dst + n;
nextChar = dst;
}
}
/**
* 读取单个字符。
*
* @return 作为一个整数(其范围从0到65535( 0x00-0xffff))返回,如果已到达流末尾,则返回 -1
*/
public int read() throws IOException {
synchronized (lock) {
//确认BufferedReader是否处于开启状态
ensureOpen();
//???
for (;;) {
//如果缓冲区数据已被读完,填充缓冲区。如果填充缓冲区后缓冲区依然是空的,说明已到达流末尾,返回-1。
if (nextChar >= nChars) {
fill();
if (nextChar >= nChars)
return -1;
}
//如果skipLF为true,说明要跳过换行符
if (skipLF) {
//先将skipLF置为false。???
skipLF = false;
//如果缓冲区内下个字符为换行符,跳过它。
if (cb[nextChar] == '\n') {
nextChar++;
continue;
}
}
//返回缓冲区中下个字符,然后nextChar+1
return cb[nextChar++];
}
}
}
/**
* 从缓冲区中读取数据,写入到cbuf中。off为开始存储字符处的偏移量。len为要读取的最大字符数。
* 如有必要,从底层输入流中读取数据。
*
* 该方法在read(char cbuf[], int off, int len)中被调用
*
* @param b 目标字符数组
* @param off 开始存储字符处的偏移量
* @param len 要读取的最大字符数
* @return 实际读入cbuf的总字节数,如果由于已到达流末尾而不再有数据,则返回-1。
*/
private int read1(char[] cbuf, int off, int len) throws IOException {
//如果缓冲区已被读完
if (nextChar >= nChars) {
//如果要读取的长度大于等于缓冲区大小,且没有标记,且不跳过换行符
if (len >= cb.length && markedChar <= UNMARKED && !skipLF) {
//直接从底层输入流中读取数据到cbuf中
return in.read(cbuf, off, len);
}
//填充缓冲区
fill();
}
//如果以上步骤执行完后,缓冲区还是没有可读数据,说明已经到达流末尾,返回-1
if (nextChar >= nChars) return -1;
//如果跳过换行符
if (skipLF) {
//将skipLF置为false。why???
skipLF = false;
//如果下个字符是换行符
if (cb[nextChar] == '\n') {
//跳过它
nextChar++;
//如果缓冲区已被读完,填充缓冲区
if (nextChar >= nChars)
fill();
//如果填充缓冲区后,缓冲区依然是空的。说明已经到达流末尾,返回-1
if (nextChar >= nChars)
return -1;
}
}
//计算实际应该读取的字节数
int n = Math.min(len, nChars - nextChar);
//读取
System.arraycopy(cb, nextChar, cbuf, off, n);
//缓冲区当前位置+n
nextChar += n;
return n;
}
/**
* 从缓冲区中读取数据,写入到cbuf中。off为开始存储字符处的偏移量。len为要读取的最大字符数。
* 如有必要,从底层输入流中读取数据。
*
* @param b 目标字符数组
* @param off 开始存储字符处的偏移量
* @param len 要读取的最大字符数
* @return 实际读入cbuf的总字节数,如果由于已到达流末尾而不再有数据,则返回-1。
*/
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;
}
//如果参数合法,调用read1(char[] cbuf, int off, int len)方法读取数据
int n = read1(cbuf, off, len);
//如果没有读取到数据,直接返回读取结果
if (n <= 0) return n;
//当没有读取到len个字符,且还可以继续读取
while ((n < len) && in.ready()) {
//继续尝试读取
int n1 = read1(cbuf, off + n, len - n);
if (n1 <= 0) break;
n += n1;
}
return n;
}
}
/**
* 读取一行字符。
* 通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r') 或回车后直接跟着换行。
*
* @param ignoreLF 是否忽略换行符。如果为true,遇到换行符时,跳过它。
*
* @return 包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回
* @see java.io.LineNumberReader#readLine()
* @exception IOException If an I/O error occurs
*/
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')或回车后直接跟着换行。
*
* @return 包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回null
*/
public String readLine() throws IOException {
return readLine(false);
}
/**
* 跳过字符。
*
* @param n 要跳过的字符数
*
* @return 实际跳过的字符数
*/
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++;
}
}
//d为缓冲区中剩余可读的字符数
long d = nChars - nextChar;
//如果r<=d,即n<=d,即要跳过的字符数小于等于缓冲区中剩余可读的字符数。直接跳过r个字符即可
if (r <= d) {
nextChar += r;
r = 0;
break;
}//如果要跳过的字符数大于缓冲区中剩余可读的字符数,
//跳过可跳的n个字符,此时缓冲区没有数据可读
else {
r -= d;//r为剩下需要跳过的字符数。
nextChar = nChars;//跳过可跳的n个字符,此时缓冲区没有数据可读
}
//此时,一次循环就结束了。下次循环会判断r是否大于0,如果大于0,继续循环。
}
//返回实际跳过的字符数。
return n - r;
}
}
/**
* 判断此流是否已准备好被读取。
* 如果缓冲区不为空,或者底层字符流已准备就绪,则缓冲的字符流准备就绪。
*/
public boolean ready() throws IOException {
synchronized (lock) {
ensureOpen();
//如果需要跳过换行符,且下个字符是换行符,那就跳过它
if (skipLF) {
//如果缓冲区已被读完且底层缓冲区已准备好被读取,就填充缓冲区
if (nextChar >= nChars && in.ready()) {
fill();
}
//如果缓冲区还有数据可读
if (nextChar < nChars) {
//跳过换行符
if (cb[nextChar] == '\n')
nextChar++;
//why???
skipLF = false;
}
}
//如果缓冲区不为空,或者底层字符流已准备就绪,则缓冲的字符流准备就绪。
return (nextChar < nChars) || in.ready();
}
}
/**
* 判断此流是否支持mark()操作
*/
public boolean markSupported() {
return true;
}
/**
* 标记流中的当前位置。
* 对reset()的后续调用将尝试将该流重新定位到此点。
*/
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;
}
}
/**
* 将流重置到最新的标记。
*/
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;
try {
in.close();
} finally {
in = null;
cb = null;
}
}
}
/**
* 返回一个Stream,其中的元素是从BufferedReader中读出的行。
*
* @since 1.8
*/
public Stream lines() {
Iterator iter = new Iterator() {
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);
}
}
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import org.junit.Test;
/**
* BufferedReader Demo
*/
public class BufferedReaderTest {
@Test
public void test() {
try {
File file = new File("bufferedReader.txt");
BufferedReader in = new BufferedReader(new FileReader(file));
System.out.println("从缓冲区中读取5个字符,并打印");
for (int i = 0; i < 5; i++) {
if (in.ready()) {
int tmp = in.read();
System.out.println(i + ":" + (char) tmp);
}
}
char[] buf = new char[3];
in.read(buf, 0, 3);
System.out.println("从缓冲区中读取三个字符存到字符数组中,字符数组内容为" + String.valueOf(buf));
System.out.println("当前行剩余字符为" + in.readLine());
System.out.println("测试BufferedReader是否支持标记");
if (!in.markSupported()) {
System.out.println("make not supported!");
return;
}
System.out.println("make supported!");
System.out.println("测试标记功能(当前位置的下个字符为A)");
in.mark(1024);
System.out.println("跳过22个字符");
in.skip(22);
System.out.println("当前位置为" + (char) in.read());
System.out.println("重置输入流的索引为mark()所标记的位置,即重置到f处");
in.reset();
System.out.println("当前位置为" + (char) in.read());
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
bufferedReader.txt内容为
abcdefghijk
ABCDEFGHIJK
1234124254
?$%^&*(*^%
运行程序后,控制台打印内容为
从缓冲区中读取5个字符,并打印
0:a
1:b
2:c
3:d
4:e
从缓冲区中读取三个字符存到字符数组中,字符数组内容为fgh
当前行剩余字符为ijk
测试BufferedReader是否支持标记
make supported!
测试标记功能(当前位置的下个字符为A)
跳过22个字符
当前位置为4
重置输入流的索引为mark()所标记的位置,即重置到f处
当前位置为A
为了更好地理解BufferedWriter的源码,我们先来了解其思想。BufferedWriter是如何为其他输出流提供缓冲功能的?提供缓冲为什么能实现单个字符、数组和字符串的高效写入?
先来看看第一次问题,BufferedWriter是如何为其他输出流提供缓冲功能的?
创建BufferedWriter时,我们会通过它的构造函数指定某个底层字符输出流Writer为参数。当程序中每次将单个字符、数组和字符串写入到BufferedWriter中时、都会检查BufferedWriter中的缓存区是否存满,如果没有存满则将字符写入到缓存区中;如果存满,则调用底层的writer(char[] b, int off, int len)将缓存区中的所有字符一次性写入到底层Writer中。
提供缓冲为什么能实现单个字符、数组和字符串的高效写入?
如果没有缓冲,使用底层字符输出流向目的文件中写入单个字符、数组和字符串时,每写入一次就要打开一次到目的文件的连接。这样频繁的访问效率非常底下。比如,将一个非常大的数据写入到目的文件中,如果每写入一个字符就要打开一次到目的文件的连接,可以想象效率是如何低下。
有了缓冲,当使用底层字符输出流向目的文件中写入单个字符、数组和字符串时,将单个字符、数组和字符串先写入到BufferedWriter的内置缓存空间中,然后当达到一定数量时一次性写入Writer流中。此时,Writer就可以打开一次通道,将这个数据块写入到文件中。这样虽然不能达到一次访问就将所有数据写入磁盘中的效果,但也大大提高了效率和减少了对磁盘的访问量。
下面来学习下BufferedWriter的源码,看看BufferedWriter是如何为其他输出流提供缓冲功能,实现单个字符、数组和字符串的高效写入的。
public class BufferedWriter extends Writer {
//底层字符输出流
private Writer out;
//缓冲区
private char cb[];
//缓冲区大小。缓冲区当前位置的下个字符。
private int nChars, nextChar;
//默认字符缓冲区大小
private static int defaultCharBufferSize = 8192;
/** 行分隔符 **/
private String lineSeparator;
/**
* 创建一个使用默认大小输出缓冲区的缓冲字符输出流。 */
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"));
}
/** 确认输出流处于打开状态。如果底层输出流不为null,则认为流处于打开状态 */
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;
}
}
/**
* 向缓冲区中写入单个字符
*/
public void write(int c) throws IOException {
synchronized (lock) {
ensureOpen();
//如果缓冲区已满,刷新缓冲区
if (nextChar >= nChars)
flushBuffer();
//向缓冲区中写入单个字符
cb[nextChar++] = (char) c;
}
}
/**
* 取最小值
*/
private int min(int a, int b) {
if (a < b) return a;
return b;
}
/**
* 写入字符数组的某一部分。
* 将字符数组的cbuf中从下标off开始的len个字符写入缓冲区cb中
*/
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;
}
//如果cbuf的数据的长度大于等于缓冲区长度
if (len >= nChars) {
//刷新缓冲区,将要写入的数据直接写入到底层输出流中
flushBuffer();
out.write(cbuf, off, len);
return;
}
//如果cbuf的数据的长度小于缓冲区长度,将数据写入缓冲区中。
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中从下标off开始的len个字符写入缓冲区cb中
*/
public void write(String s, int off, int len) throws IOException {
synchronized (lock) {
ensureOpen();
//???不需要判断参数是否合法
//将字符串s中从下标off开始的len个字符写入缓冲区cb中
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();
}
}
}
/**
* 写入一个行分隔符。
* 行分隔符字符串由系统属性line.separator定义,并且不一定是单个新行('\n')符。
*/
public void newLine() throws IOException {
write(lineSeparator);
}
/**
* 刷新该流的缓冲。
*/
public void flush() throws IOException {
synchronized (lock) {
flushBuffer();
out.flush();
}
}
//关闭此流。关闭前先刷新它。
@SuppressWarnings("try")
public void close() throws IOException {
synchronized (lock) {
if (out == null) {
return;
}
try (Writer w = out) {
flushBuffer();
} finally {
out = null;
cb = null;
}
}
}
}
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import org.junit.Test;
/**
* BufferedWriter Demo
*/
public class BufferedWriterTest {
@Test
private static void test() {
try {
File file = new File("bufferedWriter.txt");
BufferedWriter writer = new BufferedWriter(new FileWriter(file));
writer.write(new char[] { 'a', 'b', 'c', 'd', 'e' }, 0, 3);
writer.newLine();
writer.write("ABCDEFGHIJKLMN", 0, 3);
writer.newLine();
writer.write('\n');
writer.write(1200);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行测试方法后,bufferedWriter.txt内容为
abc
ABC
?
关于BufferedReader与BufferedWriter就讲到这里,想了解更多内容请参考
版权声明 |
---|
作者:潘威威 原文地址:CSDN博客-潘威威的博客-http://blog.csdn.net/panweiwei1994/article/details/78348034 本文版权归作者所有,欢迎转载。转载时请在文章明显位置给出原文作者名字(潘威威)及原文链接。请勿将本文用于任何商业用途。 |