[重学Java基础][Java IO流][Part.2]缓冲字符输入输出流
[重学Java基础][JavaIO流][Part.2]缓冲字符输入输出流
BufferedReader
概述
BufferedReader缓冲字符数组输入流,继承了所有字符输入流的超类Reader类,自带缓存区,可以一次读入缓冲区大小的字符内容,并且提供了按行读取功能
通过包装Reader对象来发挥作用
很明显 这是一个处理流 包装流 一般包装InputStreamReader,FileReader对象
官方注释
从一个输入的字符流中读取文本,为字符、数组、一行文本的高效读取提供字符缓冲功能。
缓冲区的大小可能是特殊设定值,可能是使用默认的大小。默认的大小已经足够解决大部分问题。
一般情况下,每一个read请求由一个Reader类发起,使底层进行对字节流或byte流执行相应的读取请求。
对一些read()函数开销较大的的Reader类,(例如:FileReaders和InputStreamReaders)使用BufferedReader类进行封装是很明智的。
例:
BufferedReader in = new BufferedReader(new FileReader("file.in"));
如果没由进行缓冲,对read()和readline()调用会直接从文件中读取一次byte数据,转换成字符格式并返回,这是十分不明智的做法。
程序对文本数据使用DataInputStream时,在合适的情况下可以局部替换DataInputStream为BufferedReader
源码分析
成员属性
public class BufferedReader extends Reader {
读取的数据源
private Reader in;
缓冲字符数组 流的实际内容体
private char cb[];
nChars 缓冲区中字符总个数
nextChar 下一个要读取的字符位置
private int nChars, nextChar;
“标记无效”的标志
private static final int INVALIDATED = -2;
未设置标记
private static final int UNMARKED = -1;
默认情况下无标记字符 所以markedChar是未设置标记
private int markedChar = UNMARKED;
可标记位置能标记的最大长度 只有先设置了markedChar后此变量才能生效
private int readAheadLimit = 0;
是否跳过换行字符 就是是否忽略换行 默认不忽略
private boolean skipLF = false;
设置标记时 是否忽略换行
private boolean markedSkipLF = false;
默认缓冲字符数组大小
private static int defaultCharBufferSize = 8192;
默认每一行的字符个数
private static int defaultExpectedLineLength = 80;
……
}
成员方法
- 构造方法
接受一个输入流对象 并按照入参sz的大小创建字符缓冲区
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;
}
接受一个输入流大小 并创建默认大小的字符缓冲区
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
填入缓冲区方法 真正将数据填入缓冲区的方法 其他读取方法都是此方法的装饰器
缓冲区没有数据时,通过fill()法以向缓冲区填充数据 缓冲区数据被读完,需更新时,通过fill()可以更新缓冲区的数据
private void fill() throws IOException {
缓冲数组填充的开始位置
int dst;
if (markedChar <= UNMARKED) {
无标记的情况下 默认初始位置为缓冲区头部位置0
dst = 0;
} else {
如果有标记 则标记区间长度delta 等于下一个要读取的位置与被标记的位置的距离
int delta = nextChar - markedChar;
if (delta >= readAheadLimit) {
若标记区间长度delta大于读取的限制(超过了标记上限,
即流下次读取的位置已经超过了标记位置) 则标记无效
markedChar = INVALIDATED;
readAheadLimit = 0;
dst = 0;
} else {
if (readAheadLimit <= cb.length) {
若未超过标记上限,则将下一个读取位置到标记位置开始的流内容复制到cb
System.arraycopy(cb, markedChar, cb, 0, delta);
markedChar = 0;
dst = delta;
} else {
如果标记上限大于缓冲流的总长度 则将下一个读取位置到标记位置开始的
流内容复制到新的字符数组ncb中 再将cb设置为ncb
char ncb[] = new char[readAheadLimit];
System.arraycopy(cb, markedChar, ncb, 0, delta);
cb = ncb;
markedChar = 0;
dst = delta;
}
nextChar = nChars = delta;
}
}
int n;
do {
从输入流in中读取数据 并存储到cb中 读取长度为cb.length-dst
如果没读到就继续读取
n = in.read(cb, dst, cb.length - dst);
} while (n == 0);
if (n > 0) {
如果从输入流in中读到了数据,则设置nChars(cb中字符的数目)=dst+n,
并且nextChar(下一个被读取的字符的位置)=dst。
nChars = dst + n;
nextChar = dst;
}
}
从数据源读取数据方法 每次读入一个字符
public int read() throws IOException {
线程加锁 说明读取时线程安全的
synchronized (lock) {
ensureOpen();
for (;;) {
若下一个读取的数据位置大于缓冲区的大小(缓冲区数据全部读取)
则先用fill()按方法刷新缓冲区 再读取
if (nextChar >= nChars) {
fill();
刷新后缓冲区仍然没有新数据 则返回-1 读入结束
if (nextChar >= nChars)
return -1;
}
是否忽略换行符
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '\n') {
nextChar++;
continue;
}
}
return cb[nextChar++];
}
}
}
从数据源读取数据方法 每次读入指定长度字符内容
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;
}
从数据源读取数据方法 每次读入一行内容
readLine()方法实际上调用了一个包内部方法 readLine(boolean ignoreLF)
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) {
if (s != null && s.length() > 0)
return s.toString();
else
return null;
}
boolean eol = false;
char c = 0;
int i;
是否跳过一个换行符
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);
}
}
}
代码示例
例1 按行读入文件中字符 注意文件必须是UTF-8格式的 否则会乱码
BufferedReader br = new
BufferedReader(new FileReader("d:/a.txt"));
String str;
while((str=br.readLine())!=null)
{
System.out.println(str);
}
输出
第一行 四月是你的谎言
第二行 比宇宙更远的地方
第三行 不死者之王
第四行 关于我的女友是个一本正经的碧池这件事
例2 按字符读入并转换
BufferedReader br = new BufferedReader(new FileReader("d:/a.txt"));
int c;
while((c=br.read())!=-1)
{
System.out.print((char)c);
}
结果同上
例3 标记并重置
BufferedReader br = new BufferedReader(new FileReader("d:/a.txt"));
注意这里的读入限制为大于读入字符数 读入字符数为55 小于等于这个值reset时会报错
br.mark(100);
String str;
while((str=br.readLine())!=null)
{
System.out.println(str);
}
br.reset();
int c;
while((c=br.read())!=-1)
{
System.out.print((char)c);
}
输出
第一行 四月是你的谎言
第二行 比宇宙更远的地方
第三行 不死者之王
第四行 关于我的女友是个一本正经的碧池这件事
第一行 四月是你的谎言
第二行 比宇宙更远的地方
第三行 不死者之王
第四行 关于我的女友是个一本正经的碧池这件事
典型错误
1.
BufferedReader br = new BufferedReader(new FileReader("d:/a.txt"));
String str;
while((str=br.readLine())!=null)
{
System.out.println(br.readLine());
}
输出
第二行 比宇宙更远的地方
第四行 关于我的女友是个一本正经的碧池这件事
隔一行丢失了一行数据 因为流被读取后游标移动 不会重复读入 所以只能按例1方式操作
BuffererWriter
概述
BufferedWriter缓冲字符数组输出流,继承了所有字符输出流的超类Writer类,自带缓存区,可以一次写出缓冲区大小的字符内容,并且提供了按行写出功能
通过包装Writer对象来发挥作用
很明显 这是一个处理流 包装流 一般包装InputStreamWriter,FileWriter对象
官方注释
向字符输出流写入文本时,对字符进行缓冲处理是一种高效的处理方式,尤其是逐个的字符写入,数组结构写入或者字符串结构写入
缓冲区的大小可被设定,默认大小足以满足一般情况的需求
提供了newLine()方法,提供了基于不同平台的行分割方法,因为不是所有平台都是用’\n’作为换行符,所以使用newLine方法是首选。
一般情况下,Writer类总是立即将数据输出到底层的字符流或比特流中。除非程序需要即刻输出,否则使用BufferedWriter封装时最好的选择。(对于write()方法开销较大的Writer类十分必要)
例如:
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));
对PrintWriter类进行了缓冲。如果没有缓冲,任意一个对print方法的调用将会使字符转化为byte并且立刻写入到文件中,这是非常低效率的。
源码分析
成员属性
输出的数据汇
private Writer out;
缓冲字符数组
private char cb[];
下个读入字符游标位置nextChar 缓冲区最大长度nChars
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 文本换行符是通过底层方法创建的
按当前系统的方式创建分本换行符
lineSeparator = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("line.separator"));
}
刷新输出流 可以看到刷新方法flush()内部调用了包级方法flushBuffer()方法
flushBuffer()方法则真正实现了将缓冲区字符数组写入到输出流的操作
public void flush() throws IOException {
synchronized (lock) {
flushBuffer();
out.flush();
}
}
void flushBuffer() throws IOException {
synchronized (lock) {
ensureOpen();
if (nextChar == 0)
return;
out.write(cb, 0, nextChar);
nextChar = 0;
}
}
换行方法 写入一个换行符 避免显示输出换行符可能引起的一些问题
public void newLine() throws IOException {
write(lineSeparator);
}
写入方法 读入字符数组缓冲内容 并写入到底层流
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;
}
如果写入的字符数组大于缓冲区大小 则刷新缓冲区 并直接写入下层流
可以看到是直接调用底层流out的写入方法
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();
}
}
}
代码示例
向控制台写入字符
BufferedReader br = new BufferedReader(new FileReader("d:/b.txt"));
BufferedWriter bwo=new BufferedWriter(new PrintWriter(System.out));
String str;
while((str=br.readLine())!=null)
{
bwo.write(str);
bwo.newLine();
}
bwo.flush();
输出结果
1.紫罗兰永恒花园
2.龙王的工作!
3.Fate/EXTRA Last Encore
4.citrus~柑橘味香气~
写入到文件
BufferedWriter bw=new BufferedWriter(new FileWriter("d://c.txt"));
List<String> list= Arrays.asList("比宇宙更远的地方","魔卡少女樱 CLEAR CARD篇","卫宫家今天的饭","博多豚骨拉面团");
for (String s : list) {
bw.write(s,0,s.length());
bw.newLine();
}
bw.flush();
输出结果
比宇宙更远的地方
魔卡少女樱 CLEAR CARD篇
卫宫家今天的饭
博多豚骨拉面团
注意 输出到文件 重复执行会覆盖上一次执行的内容
推荐阅读
-
java基础回顾之 流(BufferedReader/BufferedWriter )字符缓冲流, 默认有8192个字符的缓冲区
-
[重学Java基础][Java IO流][Part.2]缓冲字符输入输出流
-
【java基础】IO-Part3-字符流
-
Java学习基础部分【二十一】IO流&字符流其他内容&递归
-
java基础之--缓冲流-字节流+字符流+转换流+序列化+打印流
-
java: ----- 多线程、IO流、字符流(Buffered缓冲加强版)Copy
-
《java基础》——IO流(输入输出)
-
Java基础之IO流(字符流)
-
Java IO流之字符缓冲流实例详解
-
Java IO流之字符缓冲流实例详解