设计Reader和Writer继承层次结构的主要目的是为了国际化,老的I/O流继承层次结构仅仅支持8位字节流并且不能很好的处理16位的Unicode字符,但是新类的采用的设计比旧类库更快,因此我们应该尽可能了的使用Reader 与Writer 。
我们首先还是先看看他的继承体系
我们首先分析父类 Reader的定义:
/**
* 首先Reader是所有基于字符流操作的基类,其实现 Readable,以及Closeable接口
* 其中 Readable接口中的read()方法实现了将字符串读入charBuffer中,但是只有在需要输出的时候才会调用。
*
* 用于读取字符流的抽象类。子类必须实现的方法只有 read(char[], int, int) 和 close()。
* 但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。
*/
public abstract class Reader implements Readable, Closeable {
protected Object lock;
protected Reader() { //受保护的构造方法
this.lock = this; //用于初始化锁
}
protected Reader(Object lock) { //使用锁的构造方法
if (lock == null) {
throw new NullPointerException();
}
this.lock = lock; //初始化锁
}
// 将输入流管道中的target.length个字节读取到指定的缓存字符数组target中、返回实际存放到target中的字符数,实际为Readable定义的方法
public int read(java.nio.CharBuffer target) throws IOException {
int len = target.remaining(); // target.remaining() 返回当前流位置与流最大值之间的差值(实际就是剩余的字符数)
char[] cbuf = new char[len]; //声明一个字符数组
int n = read(cbuf, 0, len); //从流中读取 0-len个字符到cbuf中
if (n > 0) //如果写入的字符数大于0
target.put(cbuf, 0, n); //将给定字符写入此缓冲区的当前位置,然后该位置递增。
return n; //返回写入的实际数量
}
public int read() throws IOException { //读取一个字符的数据
char cb[] = new char[1]; //声明一个字符数组
if (read(cb, 0, 1) == -1) //读取一个字符到cb中
return -1;
else
return cb[0]; //返回读取的数据
}
abstract public int read(char cbuf[], int off, int len) throws IOException;
private static final int maxSkipBufferSize = 8192; //最大的字符丢弃数
private char skipBuffer[] = null; //丢弃字符缓冲器
public long skip(long n) throws IOException {
//丢弃字符实现
}
}
该类的源码似乎没有什么需要注意的地方,相对于InputStream而已,仅仅是读取的是字符而已。
再来针对其子类一个一个的看,首先是
BufferedReader:
作用:这个类就是一个包装类,它可以包装字符流,将字符流放入缓存里面,等待缓存满了或者调用flush的时候,在读入到内存,就是为了提供读的效率而设计的。
先来看看其定义:
/**
* BufferReader的作用是为其它Reader提供缓冲功能。创建BufferReader时,我们会通过它的构造函数指定某个Reader为参数。
* BufferReader会将该Reader中的数据分批读取,每次读取一部分到缓冲中;操作完缓冲中的这部分数据之后,再从Reader中读取下一部分的数据。
* 为什么需要缓冲呢?原因很简单,效率问题!缓冲中的数据实际上是保存在内存中,而原始数据可能是保存在硬盘或NandFlash中;
* 而我们知道,从内存中读取数据的速度比从硬盘读取数据的速度至少快10倍以上。那干嘛不干脆一次性将全部数据都读取到缓冲中呢?
* 第一,读取全部的数据所需要的时间可能会很长。第二,内存价格很贵,容量不想硬盘那么大。
*/
public class BufferedReader extends Reader {
private Reader in; //包装维护类 Reader
private char cb[]; //字符缓冲区
private int nChars, nextChar; //nChars 是cb缓冲区中字符的总的个数 ,nextChar 是下一个要读取的字符在cb缓冲区中的位置
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 */ //标记能标记位置的最大长度
/** If the next character is a line feed, skip it */
private boolean skipLF = false; //skipLF(即skip Line Feed)是“是否忽略换行符”标记
/** The skipLF flag when the mark was set */
private boolean markedSkipLF = false; //设置“标记”时,保存的skipLF的值
private static int defaultCharBufferSize = 8192; //默认字符缓冲区大小
private static int defaultExpectedLineLength = 80; //默认每一行的字符个数
public BufferedReader(Reader in, int sz) { //创建“Reader”对应的BufferedReader对象,sz是BufferedReader的缓冲区大小
this.in = in;
cb = new char[sz];
nextChar = nChars = 0;
}
//使用默认的缓冲区大小8192 创建BufferedReader对象
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
//确保“BufferedReader”是打开状态
private void ensureOpen() throws IOException {
if (in == null)
throw new IOException("Stream closed");
}
/**
* 填充缓冲区函数。有以下两种情况被调用:
* 缓冲区没有数据时,通过fill()可以向缓冲区填充数据。
* 缓冲区数据被读完,需更新时,通过fill()可以更新缓冲区的数据。
*/
private void fill() throws IOException {
int dst; // 表示“cb中填充数据的起始位置”。
if (markedChar <= UNMARKED) {
/* No mark */
dst = 0; //没有标记的情况,则设dst=0。
} else {
/* Marked */
int delta = nextChar - markedChar; //delta表示“当前标记的长度”,它等于“下一个被读取字符的位置”减去“标记的位置”的差值;
if (delta >= readAheadLimit) { //若“当前标记的长度”超过了“标记上限(readAheadLimit)
/* Gone past read-ahead limit: Invalidate mark */
markedChar = INVALIDATED;
readAheadLimit = 0; //丢弃标记
dst = 0;
} else {
if (readAheadLimit <= cb.length) {
/* Shuffle in the current buffer */
// 若“当前标记的长度”没有超过了“标记上限(readAheadLimit)”,
// 并且“标记上限(readAheadLimit)”小于/等于“缓冲的长度”;
// 则先将“下一个要被读取的位置,距离我们标记的置符的距离”间的字符保存到cb中。
System.arraycopy(cb, markedChar, cb, 0, delta);
markedChar = 0;
dst = delta;
} else {
// 若“当前标记的长度”没有超过了“标记上限(readAheadLimit)”,
// 并且“标记上限(readAheadLimit)”大于“缓冲的长度”;
// 则重新设置缓冲区大小,并将“下一个要被读取的位置,距离我们标记的置符的距离”间的字符保存到cb中。
/* Reallocate buffer to accommodate read-ahead limit */
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) { // 1.方法是同步的
//2.使用的是贪婪读,除非读到的字符数符合要求或者已经读完,否则一直读
while ((n < len) && in.ready()) {
int n1 = read1(cbuf, off + n, len - n);
}
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) { /* 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);
}
}
}
//读取一行数据。不忽略换行符
public String readLine() throws IOException {
return readLine(false);
}
public boolean ready() throws IOException {
synchronized (lock) { //同步方法
ensureOpen();
if (skipLF) { //是否忽略换行符
if (nextChar >= nChars && in.ready()) {
fill();
}
if (nextChar < nChars) {
if (cb[nextChar] == '\n') //则判断下一个符号是否是换行符,若是的话,则忽略
nextChar++;
skipLF = false;
}
}
return (nextChar < nChars) || in.ready();
}
}
/**
* JDK1.8新支持,可以用来读取多行文件,其返回的是一个Stream
* Stream为JDK1.8的新特性。
* @return
*/
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();
}
}
};
//通过实现的迭代器返回一个Stream对象
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
iter, Spliterator.ORDERED | Spliterator.NONNULL), false);
}
}
如果仔细看的话,可以发现,和上一盘文章中的缓冲字节流在实现上没有什么太大的区别,另外值得注意的就是,在JDK1.8引入了新的功能,也就是lines() 方法,通过该方法可以获取输入流文件的Stream,然后通过对Stream的操作获取文件内容。
上述代码中的fill()方法的详解可以参考文章
http://blog.youkuaiyun.com/asivy/article/details/18704449
再来看看CharArrayReader
/**
* CharArrayReader实际上是通过“字符数组”去保存数据。
*/
public class CharArrayReader extends Reader {
protected char buf[]; //字符缓冲区
protected int pos; //buf中下一个要被读取的字符位置
protected int markedPos = 0; //buf中被mark的字符下标
protected int count; //此缓冲区结束的索引。
public CharArrayReader(char buf[]) { //通过一个数组来初始化
this.buf = buf; //初始化缓冲区大小
this.pos = 0; //设置位置
this.count = buf.length; //设置最大数量
}
public CharArrayReader(char buf[], int offset, int length) { //通过偏移量来设置初始化
this.buf = buf;
this.pos = offset;
this.count = Math.min(offset + length, buf.length);
this.markedPos = offset;
}
private void ensureOpen() throws IOException {
}
public int read() throws IOException {
synchronized (lock) {
ensureOpen();
if (pos >= count)
return -1;
else
return buf[pos++]; //返回缓冲区中的数据
}
}
public int read(char b[], int off, int len) throws IOException {
synchronized (lock) {
//也是从缓冲区中读取,然后修改了相应的成员变量
System.arraycopy(buf, pos, b, off, len);
pos += len;
return len;
}
}
//用于判断是否还有下一个可读字符
public boolean ready() throws IOException {
synchronized (lock) {
ensureOpen();
return (count - pos) > 0; //实际就是索引的位置是否超出了count的大小
}
}
public void close() {
buf = null; //实际就是清空buf释放内存
}
}
同样,此源码仍旧让我疑惑的地方在于buf数组是在何处初始化的。
再来看看FilterReader
由于FIlterReader的设计初衷就是为了给所有的Reader包装类用作基类,因而其本身没有太多值得关注的地方,到目前为止,其只有一个子类
PushBackReader可回退的字符流
我们稍微看一下源码如下:
/**
* 本类是用作所有Reader装饰类的基类,因而本身没有什么太多特别需要注意的地方
* 其实现几乎都是通过委托其维护的成员变量Reader来实现的,
* 目前该类只有一个子类就是PushBackReader (可回退字符读)
*/
public abstract class FilterReader extends Reader {
protected Reader in;
protected FilterReader(Reader in) { //通过Reader类来初始化
super(in);
this.in = in;
}
public int read() throws IOException {
return in.read();
}
public int read(char cbuf[], int off, int len) throws IOException {
return in.read(cbuf, off, len);
}
public long skip(long n) throws IOException {
return in.skip(n);
}
public boolean ready() throws IOException {
return in.ready();
}
public boolean markSupported() {
return in.markSupported();
}
public void mark(int readAheadLimit) throws IOException {
in.mark(readAheadLimit);
}
public void reset() throws IOException {
in.reset();
}
public void close() throws IOException {
in.close();
}
}
再来看看InputStreamReader
通过继承系统,我们可以知道其,还有一个子类FileReader,我们留到后面再来分析他,首先先看看定义
/**
* 通过名字应该可以感受出其似乎既有读取字节流(InputStream)的功能,又有读取字符流的功能(Reader)
* 作用:是字节流与字符流之间的桥梁,能将字节流输出为字符流,并且能为字节流指定字符集,可输出一个个的字符;
* 当使用字节码初始化该类后,在调用该类的读方法获取的就是字符了
* 我们的机器只会读字节码,而我们人却很难读懂字节码,所以人与机器交流过程中需要编码解码
* InputStreamReader及其子类FileReader:(从字节到字符)是个解码过程;
* InputStreamReader这个解码过程中,最主要的就是StreamDecoder类
*/
public class InputStreamReader extends java.io.Reader {
private final StreamDecoder sd; //该对象是解码也就是将字节解码成字符的实现类
public InputStreamReader(InputStream in) { //构造方法需要一个字节流
super(in);
try {
//初始化解码器
sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
} catch (UnsupportedEncodingException e) {
// The default encoding should always be available
throw new Error(e);
}
}
public InputStreamReader(InputStream in, String charsetName) //输入流+编码
throws UnsupportedEncodingException
{
super(in);
if (charsetName == null)
throw new NullPointerException("charsetName");
sd = StreamDecoder.forInputStreamReader(in, this, charsetName); //指定编码的解码器
}
public String getEncoding() { //得到解码器的编码
return sd.getEncoding();
}
public int read() throws IOException {
return sd.read(); //字节委托给解码器,读出来的是字符
}
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length); //同上
}
public boolean ready() throws IOException {
return sd.ready();
}
public void close() throws IOException {
sd.close();
}
}
通过对源码的解析,似乎也没有什么特别值得注意的,唯一可能需要深究的就是里面维护的解码对象StreamDecoder了,该对象的作用就是根据指定的编码将字节流转码成字符流。
再来看看PipedReader对象
/**
* PipedReader和PipedWriter即管道输入流和输出流,主要用于线程间管道通信
*/
public class PipedReader extends Reader {
boolean closedByWriter = false; //PipedWriter是否关闭的标记
boolean closedByReader = false; //PipedReader是否关闭的标记
//PipedReader与PipedWriter是否连接的标记
//它在PipedWriter的connect()连接函数中被设置为true
boolean connected = false;
Thread readSide; // 读取“管道”数据的线程
Thread writeSide; // 向“管道”写入数据的线程
private static final int DEFAULT_PIPE_SIZE = 1024; //管道的默认大小
char buffer[]; //缓冲区
int in = -1; //下一个写入字符的位置。in==out代表满,说明“写入的数据”全部被读取了。
int out = 0; //下一个读取字符的位置。in==out代表满,说明“写入的数据”全部被读取了。
//构造函数:指定与“PipedReader”关联的“PipedWriter”
public PipedReader(PipedWriter src) throws IOException {
this(src, DEFAULT_PIPE_SIZE);
}
//构造函数:指定与“PipedReader”关联的“PipedWriter”,以及“缓冲区大小”
public PipedReader(PipedWriter src, int pipeSize) throws IOException {
initPipe(pipeSize);
connect(src);
}
//构造函数:默认缓冲区大小是1024字符
public PipedReader() {
initPipe(DEFAULT_PIPE_SIZE);
}
//构造函数:指定缓冲区大小是pipeSize
public PipedReader(int pipeSize) {
initPipe(pipeSize);
}
//初始化“管道”:新建缓冲区大小
private void initPipe(int pipeSize) {
if (pipeSize <= 0) {
throw new IllegalArgumentException("Pipe size <= 0");
}
buffer = new char[pipeSize];
}
// 将“PipedReader”和“PipedWriter”绑定。
// 实际上,这里调用的是PipedWriter的connect()函数
public void connect(PipedWriter src) throws IOException {
src.connect(this);
}
// 接收int类型的数据b。
// 它只会在PipedWriter的write(int b)中会被调用
synchronized void receive(int c) throws IOException {
// 检查管道状态
if (!connected) {
throw new IOException("Pipe not connected");
} else if (closedByWriter || closedByReader) {
throw new IOException("Pipe closed");
} else if (readSide != null && !readSide.isAlive()) {
throw new IOException("Read end dead");
}
// 获取“写入管道”的线程
writeSide = Thread.currentThread();
// 如果“管道中被读取的数据,等于写入管道的数据”时,
// 则每隔1000ms检查“管道状态”,并唤醒管道操作:若有“读取管道数据线程被阻塞”,则唤醒该线程。
while (in == out) {
if ((readSide != null) && !readSide.isAlive()) {
throw new IOException("Pipe broken");
}
/* full: kick any waiting readers */
notifyAll();
try {
wait(1000);
} catch (InterruptedException ex) {
throw new java.io.InterruptedIOException();
}
}
if (in < 0) {
in = 0;
out = 0;
}
buffer[in++] = (char) c; //将c写入到缓冲字符中
if (in >= buffer.length) {
in = 0;
}
}
// 接收字符数组c
synchronized void receive(char c[], int off, int len) throws IOException {
while (--len >= 0) {
receive(c[off++]);
}
}
// 当PipedWriter被关闭时,被调用
synchronized void receivedLast() {
closedByWriter = true;
notifyAll();
}
// 从管道(的缓冲)中读取一个字符,并将其转换成int类型
public synchronized int read() throws IOException {
//判断是否连接
if (!connected) {
throw new IOException("Pipe not connected");
} else if (closedByReader) {
throw new IOException("Pipe closed");
} else if (writeSide != null && !writeSide.isAlive()
&& !closedByWriter && (in < 0)) {
throw new IOException("Write end dead");
}
//获取读管道线程
readSide = Thread.currentThread();
int trials = 2;
while (in < 0) { //如果下一个写入的位置小于0
if (closedByWriter) { //判断写是否关闭
/* closed by writer, return EOF */
return -1;
}
if ((writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)) {
throw new IOException("Pipe broken");
}
/* might be a writer waiting */
notifyAll(); //唤醒写
try {
wait(1000); //等待1秒
} catch (InterruptedException ex) {
throw new java.io.InterruptedIOException();
}
}
int ret = buffer[out++]; //从缓冲区读出数据
if (out >= buffer.length) {
out = 0;
}
if (in == out) {
/* now empty */
in = -1;
}
return ret;
}
// 从管道(的缓冲)中读取数据,并将其存入到数组b中
public synchronized int read(char cbuf[], int off, int len) throws IOException {
if (!connected) {
throw new IOException("Pipe not connected");
} else if (closedByReader) {
throw new IOException("Pipe closed");
} else if (writeSide != null && !writeSide.isAlive()
&& !closedByWriter && (in < 0)) {
throw new IOException("Write end dead");
}
if ((off < 0) || (off > cbuf.length) || (len < 0) ||
((off + len) > cbuf.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
/* possibly wait on the first character */
int c = read();
if (c < 0) {
return -1;
}
cbuf[off] = (char)c;
int rlen = 1;
while ((in >= 0) && (--len > 0)) {
cbuf[off + rlen] = buffer[out++];
rlen++;
if (out >= buffer.length) {
out = 0;
}
if (in == out) {
/* now empty */
in = -1;
}
}
return rlen;
}
//判断是否还可以读取下一个字符
public synchronized boolean ready() throws IOException {
if (!connected) {
throw new IOException("Pipe not connected");
} else if (closedByReader) {
throw new IOException("Pipe closed");
} else if (writeSide != null && !writeSide.isAlive()
&& !closedByWriter && (in < 0)) {
throw new IOException("Write end dead");
}
if (in < 0) {
return false;
} else {
return true;
}
}
public void close() throws IOException {
in = -1;
closedByReader = true;
}
}
再来看看Reader的最后一个子类:
StringReader :作用就是提供一个String到Reader的适配器类
/**
* StringReader并不常用,因为通常情况下使用String更简单一些。
* 但是在一些需要Reader作为参数的情况下,就需要将String读入到StringReader中来使用了。
*
* StringReader和StringWrite这两个类将String类适配到了Reader和Writer接口,在StringWriter类实现的过程中,
* 真正使用的是StringBuffer,前面讲过,StringBuffer是一个可变类,由于Writer类中有许多字符串的操作,
* 所以这个类用起来比较方便;在StringReader类中只定义一个String类即可,因为只涉及到类的读取,而没有修改等的操作,
* 不会创建多个字符串而造成资源浪费。
*/
public class StringReader extends java.io.Reader {
private String str; //待转化的元素字符串
private int length;
private int next = 0; //下一个读取的字符位置
//构造参数需要一个待转化的字符串
public StringReader(String s) {
this.str = s; //初始化字符串
this.length = s.length(); //初始化字符串长度
}
public int read() throws IOException {
synchronized (lock) {
if (next >= length)
return -1;
//String.charAt() 方法返回指定索引处的char值。
return str.charAt(next++);
}
}
/**
* 将String中指定位置的字符写入到cbuf的char数组中
*/
public int read(char cbuf[], int off, int len) throws IOException {
synchronized (lock) {
int n = Math.min(length - next, len);
str.getChars(next, next + n, cbuf, off);
next += n;
return n;
}
}
//是否有下一个可读的字符 ---> 为何直接返回的就是true ?
public boolean ready() throws IOException {
synchronized (lock) {
return true;
}
}
public void close() {
str = null;
}
}
最后分析完了Reader的直接子类之后我们再来看看剩下的三个孙子类
分别是
LineNumberReader 看名字就可以知道,这个就是可以返回读取文件的行号
PushBackReader 主要作用就是一个可回退的字符读取流
FileReader 主要就是一个针对文件的读字符流