Java IO流之字符缓冲流(源码注释)【四】
1 BufferedWriter【字符缓冲输出流】
java.io.BufferedWriter
是字符缓冲输出流,能够将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。
可以指定缓冲区的大小,或者接受默认的大小(8192)。大多数情况下,默认值就足够了。
字符缓冲输出流的基本方法与普通字符输出流一致,不再赘述,我们来看它们具备的特有方法。
public void newLine() :
写入一个行分隔符。行分隔符字符串由系统属性line.separator 定义,windows为 \r\n linux为\n mac为\r
1.1 java.io.BufferedWriter源码注释
package java.io;
public class BufferedWriter extends Writer {
private Writer out;
//内部缓冲字符数组
private char cb[];
// nChars:缓冲数组容量 nextChar:下一个读取的字符索引(读取光标的位置)
private int nChars, nextChar;
private static int defaultCharBufferSize = 8192;
//行分隔符
private String lineSeparator;
// 使用默认容量构造BufferedWriter对象
public BufferedWriter(Writer out) {
this(out, defaultCharBufferSize);
}
// 使用给定容量构造BufferedWriter对象
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"));
}
// 验证流是否被关闭
private void ensureOpen() throws IOException {
if (out == null)
throw new IOException("Stream closed");
}
/**
* 将输出缓冲区刷新至系统底层,
* 此方法不是私有的,因此可以被PrintStream调用。
*/
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();
/**
* 这个操作分两步:
* 1:cb[nextChar] = (char) c;
* 2:nextChar++;
*/
cb[nextChar++] = (char) c;
}
}
/**
* 本流特有的比较大小方法,目的在于:
* 1.如果用光了描述符,能够避免加载java.lang.Math
* 2.打印对战跟踪信息
*/
private int min(int a, int b) {
if (a < b) return a;
return b;
}
/**
* 写入字符数组的某一部分。
* 一般来说,此方法将给定数组的字符存入此流的缓冲区中,根据需要刷新该缓冲区,并转到底层流。
* 但是,如果请求的长度至少与此缓冲区大小相同,则此方法将刷新该缓冲区并将各个字符直接写入底层流。
* 因此多余的 BufferedWriter 将不必复制数据。
*/
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) {
/**
* 如果请求的长度至少与此缓冲区大小相同,
* 则此方法将刷新该缓冲区并将cbuf中的字符直接写入底层流,
* (因为字符太多了,缓冲区存不下了)
*/
flushBuffer();
out.write(cbuf, off, len);
return;
}
int b = off, t = off + len;
// 通过while循环 缓存指定个数的字符
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();
}
}
}
/**
* 写入字符串的某一部分。
* 如果 len 参数的值为负数,则不写入任何字符。
* 这与超类中此方法的规范正好相反,它要求抛出 IndexOutOfBoundsException。
*/
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中的字符,从b的位置开始,到b + d的位置结束,复制到cb字符数组中,从nextChar的位置开始
s.getChars(b, b + d, cb, nextChar);
b += d;
nextChar += d;
// 如果超过缓冲区大小,就刷新至底层
if (nextChar >= nChars)
flushBuffer();
}
}
}
// 写入一个行分隔符
public void newLine() throws IOException {
// 底层调用本类的write(String s, int off, int len)方法
write(lineSeparator);
}
// 刷新
public void flush() throws IOException {
synchronized (lock) {
flushBuffer();
out.flush();
}
}
// 没有catch时的警告
@SuppressWarnings("try")
public void close() throws IOException {
//关闭此流,但要先刷新它。
synchronized (lock) {
if (out == null) {
return;
}
try (Writer w = out) {
flushBuffer();
} finally {
out = null;
cb = null;
}
}
}
}
2 BufferedReader【字符缓冲输入流】
java.io.BufferedReader
是字符缓冲输入流,能够从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
字符缓冲输入流的基本方法与普通字符输入流一致,不再赘述,我们来看它具备的特有方法。
public String readLine():读取一个文本行。通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r') 或回车后直接跟着换行。
2.1 BufferedReader源码注释
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;
/**
*从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
* 可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。
* 通常,Reader 所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。
* 因此,建议用 BufferedReader 包装所有其 read 操作可能开销很高的 Reader(如 FileReader 和 InputStreamReader)。例如,
* BufferedReader in = new BufferedReader(new FileReader("foo.in"));
* 将缓冲指定文件的输入。如果没有缓冲,则每次调用 read() 或 readLine() 都会导致从文件中读取字节,并将其转换为字符后返回,极其低效。
* 通过用合适的 BufferedReader 替代每个 DataInputStream,可以对将 DataInputStream 用于文字输入的程序进行本地化。
*/
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;
// 标记的字符位置
private int markedChar = UNMARKED;
// 预读字符的个数限制
private int readAheadLimit = 0; //仅当markedChar>0时有效
// 判断下一个字符是否是换行符,如果是,就跳过
private boolean skipLF = false;
// 设置标记时的skipLF标志
private boolean markedSkipLF = false;
// 默认字符数组容量
private static int defaultCharBufferSize = 8192;
// 默认期望的行长度
private static int defaultExpectedLineLength = 80;
// 使用给定容量构造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;
}
// 使用默认容量构造BufferedReader对象
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 {
// 数组当前容量,以及下一个读取的字符索引+1
int dst;
if (markedChar <= UNMARKED) {
// 当前流没有标记
dst = 0;
} else {
// 当前流有标记
int delta = nextChar - markedChar; //标记位置至当前位置的字符个数
if (delta >= readAheadLimit) {
/* 超过预读限制:无效标记 */
markedChar = INVALIDATED;
readAheadLimit = 0;
dst = 0;
} else {
if (readAheadLimit <= cb.length) {
/* Shuffle in the current buffer */
// 将cb数组从markedChar开始复制到自身,一共复制 标记位置至当前位置的字符个数
System.arraycopy(cb, markedChar, cb, 0, delta);
markedChar = 0;
dst = delta;
} else {
/* 重新分配缓冲区以适应预读限制 */
char ncb[] = new char[readAheadLimit];
// 将cb数组从markedChar开始复制到ncb数组中,一共复制 标记位置至当前位置的字符个数
System.arraycopy(cb, markedChar, ncb, 0, delta);
cb = ncb;
markedChar = 0;
dst = delta;
}
nextChar = nChars = delta;
}
}
int n;
do {
// 一次性将cb数组中的剩余容量填满
n = in.read(cb, dst, cb.length - dst);
} while (n == 0);
if (n > 0) {
// 设置新容量(复制后的字符个数+后来读取的字符个数)
nChars = dst + n;
// 下一个读取的字符位置保持不变
nextChar = dst;
}
}
/**
* 读取单个字符,并返回。
* 该整数是(范围从 0 到 65535 (0x00-0xffff))读入的字符,如果已到达流末尾,则返回 -1
*/
public int read() throws IOException {
synchronized (lock) {
ensureOpen();
for (;;) { // 死循环,直到遇到合适的return条件
if (nextChar >= nChars) { //下一个读取的字符超过了缓冲区的容量
fill();
if (nextChar >= nChars) //流末尾
return -1;
}
if (skipLF) { // 换行
skipLF = false;
if (cb[nextChar] == '\n') { //linux
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) {
// StreamDecoder.read(char[] var1, int var2, int var3)
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);
// 把填充满的cb数组中从nextChar开始的字符,复制到cbuf中,从off偏移开始,复制实际读取的字符个数
System.arraycopy(cb, nextChar, cbuf, off, n);
//更改下一个读取字符的位置
nextChar += n;
return n;
}
/**
* 将字符读入数组的某一部分。
* 此方法实现 Reader 类相应 read 方法的常规协定。
* 另一个便捷之处在于,它将通过重复地调用底层流的 read 方法,尝试读取尽可能多的字符。
* 这种迭代的 read 会一直继续下去,直到满足以下条件之一:
* 1.已经读取了指定的字符数,
* 2.底层流的 read 方法返回 -1,指示文件末尾(end-of-file),或者
* 3.底层流的 ready 方法返回 false,指示将阻塞后续的输入请求。
* 如果第一次对底层流调用 read 返回 -1(指示文件末尾),则此方法返回 -1。否则此方法返回实际读取的字符数。
* 鼓励(但不是必须)此类的各个子类以相同的方式尝试读取尽可能多的字符。
* 一般来说,此方法从此流的字符缓冲区中获得字符,根据需要从底层流中填充缓冲区。
* 但是,如果缓冲区为空、标记无效,并且所请求的长度至少与缓冲区相同,则此方法将直接从底层流中将字符读取到给定的数组中。
* 因此多余的 BufferedReader 将不必复制数据。
*/
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()) {
/**
* 实际读取到的字符比len小,且流对象仍然可用,就继续循环读取。知道满足下列情形:
* 1.已经读取了指定的字符数,
* 2.底层流的 read 方法返回 -1,指示文件末尾(end-of-file),或者
* 3.底层流的 ready 方法返回 false,指示将阻塞后续的输入请求。
* 举例:缓冲区容量为1000,已经存在800个字符数,那么填充缓冲区,只读取了200个。
* 但是,该方法的len是要读取500个字符,所以个数不足,仍然要继续读取。
*/
int n1 = read1(cbuf, off + n, len - n);
// 流末尾,终止循环
if (n1 <= 0) break;
n += n1;
}
// 返回实际读取的字符个数,该值 <= len
return n;
}
}
/**
* 读取一个文本行。通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r') 或回车后直接跟着换行。
* 返回该行内容的字符串,不包含行终止符,如果已到达流末尾,则返回 null
*/
String readLine(boolean ignoreLF) throws IOException {
StringBuffer s = null;
// 初始字符的位置
int startChar;
synchronized (lock) {
ensureOpen();
// 判断下个字符是否是换行符
boolean omitLF = ignoreLF || skipLF;
/** bufferLoop主要是不断地遍历底层的数组cb,并取两个换行符之间的数据给StringBuffer s。
* 当底层数组遍历完要用fill()把数据从流中填充到cb,直到流的末尾
* charloop:主要是遍历缓冲数组cb,以确定'\n','\r'的位置
* nextChar:下一个读取到的字符保存位置
* nChars:缓冲区容量
*/
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);
}
}
}
/**
* 读取一个文本行。通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r') 或回车后直接跟着换行。
* 返回该行内容的字符串,不包含行终止符,如果已到达流末尾,则返回 null
*/
public String readLine() throws IOException {
return readLine(false);
}
//跳过字符,返回实际跳过的字符个数
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 */
// 此时说明fill()已经读到流末尾
break;
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '\n') {
nextChar++;
}
}
// 获取字符数组中可读的剩余字符个数
long d = nChars - nextChar;
if (r <= d) {
// 跳过的字符个数 < 数组剩余字符字数,则跳过n个字符个数即可
nextChar += r;
r = 0;
break;
}
else {
/**
* 跳过的字符个数 > 数组剩余字符字数
* 则将数组中剩余字符数跳过,计算还应跳过的字符个数
* 继续从流中填充字符数组,继续跳过字符
*/
r -= d;
nextChar = nChars;
}
}
return n - r;
}
}
// 判断此流是否已准备好被读取。如果缓冲区不为空,或者底层字符流已准备就绪,则缓冲的字符流准备就绪。
public boolean ready() throws IOException {
synchronized (lock) {
ensureOpen();
//如果需要跳过换行符,并且读取下一个字符是换行符,那么就直接跳过它。
if (skipLF) {
/**
* 可读取的字符个数,已经到达字符数组的末尾,且流状态依然可用,则对数组进行填充
* 当且仅当下一个读取不会阻塞,in.ready()才会返回true
*/
if (nextChar >= nChars && in.ready()) {
fill();
}
if (nextChar < nChars) {
if (cb[nextChar] == '\n')
nextChar++;
skipLF = false;
}
}
return (nextChar < nChars) || in.ready();
}
}
// 判断此流是否支持 mark() 操作(它一定支持)。
public boolean markSupported() {
return true;
}
/**
* 标记流中的当前位置。对 reset() 的后续调用将尝试将该流重新定位到此点。
* 参数:readAheadLimit -
* 在仍保留该标记的情况下,对可读取字符数量的限制。在读取达到或超过此限制的字符后,尝试重置流可能会失败。
* 限制值大于输入缓冲区的大小将导致分配一个新缓冲区,其大小不小于该限制值。因此应该小心使用较大的值。
*/
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中读取的行
*/
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);
}
}
3 简单演示
请将以下文本回复原本的顺序显示。
3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
9.今当远离,临表涕零,不知所言。
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。
3.1代码示例
package com.hanyxx.io;
import java.io.*;
import java.util.Collection;
import java.util.TreeMap;
/**
* 文件排序
* @author layman
*/
public class Demo09 {
public static void main(String[] args) throws IOException {
BufferedReader fos = new BufferedReader(new FileReader("出师表.txt"));
BufferedWriter fis = new BufferedWriter(new FileWriter("出师表_copy.txt"));
TreeMap<String,String> treeMap = new TreeMap<>();
String read;
while((read = fos.readLine()) != null){
// TreeMap使用自然排序
treeMap.put(read.substring(0,1),read);
}
Collection<String> values = treeMap.values();
for (String value : values) {
// 遍历集合,换行写入
//fis.write(value);
// 写入时去除开头的行号
fis.write(value.substring(2));
fis.newLine();
}
// 关闭流
fis.close();
fos.close();
}
}