IO流中所谓的I和O实际上指的 input 和 output, 而流则是指计算机的各个部件之间的的数据流动。
根据数据的传输方向,我们可以分为输入流和输出流。
而IO流根据处理的数据类型不同,可以分为字节流、字符流、缓冲流。
而缓冲流也分为字节缓冲流、字符缓冲流。
(一般来说,我们说的IO流分类是按照数据类型来分的)
IO流就是用来处理设备数据传输问题的,常见的应用有:文件复制、文件上传、文件下载等等。
那么什么情况下使用字节流?在什么情况下使用字符流呢?
- 人类读得懂的东西,例如存有一篇文章的txt文档,这些里面的内容我们可以看懂,这样就使用字符流。
- 人类读不懂的东西,例如一张图片,用文本格式打开会看到一群乱码,因此传输图片等这种东西就可以使用字节流。
- 如果不确定使用字节流还是字符流,那么我们可以使用字节流。
需要注意的是用IO流对文件进行操作时,最后面一定要记得关闭文件!!!
说起字节流,不得不提起两个类 InputStream 和 OutputStream, 这两个类分别是输入字节流的所有类的超类和输出字节流的所有类的超类。
但因这两个超类均为抽象类,因此这两个超类无法实例化,只能依靠其子类实例化。
- InputStream 的子类:AudioInputStream , ByteArrayInputStream , FileInputStream , FilterInputStream , InputStream , ObjectInputStream , PipedInputStream , SequenceInputStream , StringBufferInputStream
- OutputStream的子类:ByteArrayOutputStream , FileOutputStream , FilterOutputStream , ObjectOutputStream , OutputStream , PipedOutputStream
子类名特点:子类名称都是以其父类名作为子类名的后缀。
本文主要讲述 FileInputStream 和 FileOutputStream 。
FileOutputStream 是文件输出流是用于将数据写入File或FileDescriptor的输出流。
要学习这个类的使用,首先第一步先从构造方法开始。
常用构造方法如下:
Constructor | 描述 |
---|---|
FileOutputStream(String name) |
创建文件输出流以指定的名称写入文件。 |
FileOutputStream(String name, boolean append) |
创建文件输出流以指定的名称写入文件。 |
FileOutputStream 常用的方法如下:
Modifier and Type | 方法 | 描述 |
---|---|---|
void |
close() |
关闭此文件输出流并释放与此流相关联的任何系统资源。 |
void |
write(byte[] b) |
将 b.length 字节从指定的字节数组写入此文件输出流。 |
void |
write(byte[] b, int off, int len) |
将 len 字节从指定的字节数组开始,从偏移量 off 开始写入此文件输出流。 |
void |
write(int b) |
将指定的字节写入此文件输出流。 |
package Stream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo {
public static void main(String[] args) throws IOException {
//创建字节输出流对象
FileOutputStream fos = new FileOutputStream("IO\\fos.txt");
/*
* 创建对象的代码实现了三步:
* 1、调用系统功能创建文件
* 2、创建字节输出流对象
* 3、让字节输出流对象指向创建好的文件
* */
byte[] bys = "Hello".getBytes();
//第一种写入方式,结果是写入Hello
fos.write(bys);
//第二种写入方式,结果是写入ello
fos.write(bys, 1, bys.length - 1);
//第三种写入方式,结果是写入a
fos.write(97);
fos.close();
//这个很重要,一定要关闭输入流
}
}
通过这代码的执行结果,我们可以发现两个事情
- 使用IO流时不仅仅需要调用IO包,并且还存在异常,需要对异常抛出或者处理程序才能编译通过
- 如果要操作的文件不存在的时候,系统将自动创建文件。并且如果文件存在,那么每一次输入的数据都会被覆盖,那么有没有一种方法是不会覆盖的呢?
首先第二个问题:
很显然,想要不覆盖之前的内容,只需要在构造方法中进行修改即可
FileOutputStream(String name, boolean append) 在这个构造方法中,若append为true, 那么对文件写入数据时候,将不再对原来的数据进行覆盖。
解决对追加写入数据的问题后,兴冲冲再去对文件进行追加写入数据后,可能会发现一个问题,为啥写入的数据都是一行的呢?写入的操作中怎么完成换行的操作呢??
要解决这个问题就得先清楚每个系统是如何实现换行的!!!
因此只需要在追加写入的数据的最后面加入换行的字符即可完成追加写入并且换行的操作!
再说说异常这个问题
如果我们选择的是抛出IOException,那么是没有什么值得注意的,但是在实际开发中,我们往往是要处理这个问题,因此需要使用try …catch…来解决异常。
package Stream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo01 {
public static void main(String[] args) {
try{
FileOutputStream fos = new FileOutputStream("IO\\fos.txt");
fos.write("Hello".getBytes());
fos.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
如果这样处理异常的话,不难发现一个问题,如果在进行write动作的时候出现了 IOException ,那么程序将会执行catch这段代码,抓住这个异常并处理;
因此write动作后面的fos.close();可能将不会被执行,这样导致的后果就是这个字节流没有被关闭,最后可能导致数据损失。
故这个**fos.close();**不应该放在write动作后面,这时候要引入一个关键词finally;
finally:在异常处理时提供finally块来执行所有清楚操作,比如说释放资源。
因此代码应改为如下更为合理:
package Stream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo01 {
public static void main(String[] args) {
FileOutputStream fos = null;
try{
fos = new FileOutputStream("IO\\fos.txt");
fos.write("Hello".getBytes());
}catch (IOException e){
e.printStackTrace();
}finally {
try {
fos.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
FileInputStream
用于读取诸如图像数据的原始字节流。同样,要学习这个类的使用,首先第一步先从构造方法开始。
常用构造方法如下:
Constructor | 描述 |
---|---|
FileInputStream(File file) |
通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File 对象 file 命名。 |
FileInputStream(String name) |
通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name 命名。 |
FileInputStream 常用方法如下:
Modifier and Type | 方法 | 描述 |
---|---|---|
void |
close() |
关闭此文件输入流并释放与流相关联的任何系统资源。 |
int |
read() |
从该输入流读取一个字节的数据。 |
int |
read(byte[] b) |
从该输入流读取最多 b.length 个字节的数据到一个字节数组。 |
int |
read(byte[] b, int off, int len) |
从该输入流读取最多 len 个字节的数据到字节数组。 |
package Stream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class FileInputStreamDemo01 {
public static void main(String[] args) throws IOException {
//创建输入流对象
FileInputStream fis = new FileInputStream("IO\\fos.txt");
/*
文件内容:
Hello
World
Java
*/
//第一种读取数据的方法(一次只读一个字节数据)
int by;
while ((by = fis.read()) != -1){
System.out.print((char)by);
}
//第二张读取数据的方法
byte[] bys = new byte[1024];
int len;
while((len = fis.read(bys)) != -1){
//String构造方法
//String(byte[] bytes, int offset, int length) 通过使用平台的默认字符集解码指定的字节子阵列来构造新的 String 。
System.out.print(new String(bys, 0, len));
}
fis.close();
}
}
通过字节流读取文件数据是一个重复的操作,因此我们可以使用一个循环进行操作,这个循环的关键就在于控制循环的条件,在read方法中,如果没有更多的数据,因为文件的结尾已经到达, 那么则会返回-1 ;否则从该输入流读取最多bys.length个字节的数据作为字节数组。
用一个len变量接受返回的数值,如果长度不为-1那么则打印出数据,以此来作为循环的约束条件。
字节缓冲流主要有两个:BufferedOutputStream 和 BufferedInputStream
- BufferedOutputStream : 该类实现缓冲输出流。 通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用;
- BufferedInputStream :创建BufferedInputStream 将会创建一个内部缓冲数组,当从流读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流重新填充,一次很多个字节。
使用字节缓冲流对象的时候,会创建一个缓冲数值,通过缓冲数组进行读和写,从而减少IO次数,进而提高读写的效率。讲白了IO就像送快递,字节流就是快递小哥一次只带一个快递,但是缓冲流就是快递小哥用车来运输快递,一次运输多个快递,效率大大提高。
构造方法如下:
Constructor | 描述 |
---|---|
BufferedOutputStream(OutputStream out) |
创建一个新的缓冲输出流,以将数据写入指定的底层输出流。 |
BufferedOutputStream(OutputStream out, int size) |
创建一个新的缓冲输出流,以便以指定的缓冲区大小将数据写入指定的底层输出流。 |
从 BufferedOutputStream 构造方法可以知道,传递过去的参数是 OutputStream类型的,因此要使用缓冲输出流之前先要创建一个字节输出流FileOutputStream对象。
常用的方法如下:
Modifier and Type | 方法 | 描述 |
---|---|---|
void |
flush() |
刷新缓冲输出流。 |
void |
write(byte[] b, int off, int len) |
从偏移量 off 开始的指定字节数组写入 len 字节到缓冲输出流。 |
void |
write(int b) |
将指定的字节写入缓冲的输出流。 |
这其中的write方法和FileOutputStream中的write方法用法大体上一样。
而flush()方法则是实现了一个刷新功能,因为输入的数据是存在一个缓冲区里面,若想吧缓冲区的数据存放到文件中,那么就需要用到flush方法,它可以刷新缓冲输入流,实现此功能。
package Stream;
import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class BufferedOutputStreamDemo01 {
public static void main(String[] args) throws IOException {
//创建字节输出缓冲流对象
BufferedOutputStream bof = new BufferedOutputStream(new FileOutputStream("IO\\bof.txt"));
//写入数据
bof.write("Hello\r\n".getBytes());
bof.write("World\r\n".getBytes());
//刷新缓冲流
bof.flush();
//释放资源
bof.close();
}
}
改程序的运行结果是将Hello和World分别写进文件的两行,这时候就会感觉奇怪了,不是说要不说明是否追加写入的话默认不追加写入吗?那为什么这里还能写进两个字符串?
这是文件关闭(执行close方法)后再打开文件写入数据,原来数据保存,那才叫追加写入。但是这里从头到尾文件只打开一次,所以不是追加写入,只是在打开一个文件的情况下,进行写入数据。
构造方法如下:
Constructor | 描述 |
---|---|
BufferedInputStream(InputStream in) |
创建一个 BufferedInputStream 并保存其参数,输入流 in 供以后使用。 |
BufferedInputStream(InputStream in, int size) |
创建具有指定缓冲区大小的 BufferedInputStream ,并保存其参数,输入流 in 供以后使用。 |
同样的,BufferedInputStream 的构造方法传递的参数类型是 InputStream 类型的数据,因此在创建这个BufferedInputStream 对象时,需要先构造一个 FileInputStream 对象,然后作为BufferedInputStream 对象构造方法的参数。
常用方法如下:
Modifier and Type | 方法 | 描述 |
---|---|---|
void |
close() |
关闭此输入流并释放与流相关联的任何系统资源。 |
int |
read() |
见 read 法 InputStream 的一般合同。 |
int |
read(byte[] b, int off, int len) |
从给定的偏移开始,将字节输入流中的字节读入指定的字节数组。 |
这些常用的方法的使用方法,与FileInputStream类中的没啥区别,用法大致相同
package Stream;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class BufferedInputStreamDemo01 {
public static void main(String[] args) throws IOException {
//创建字节缓冲输入流对象
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("IO\\bof.txt"));
//一次读取一个字节数据
int by;
while ((by = bis.read()) != -1){
System.out.print((char) by);
}
//一次读取一个字节数组
byte[] bys = new byte[1024];
int len;
while ((len = bis.read(bys)) != -1) {
System.out.println(new String(bys, 0, len));
}
bis.close();
}
}
为什么会出现字符流呢?
是因为如果读取文本数据时,如果文本数据中有中文,而中文是由多个字节构成的,那么字节流通过读取一个一个字节输出的方式显然是不行,这样输出的结果会是一串乱码。
此时字符流的优势就展现出来了,而字符流 = 字节流 + 编码表;
一个汉字在不同的编码的存储是不一样的
- 如果是GBK编码,占用2个字节
- 如果是UTF-8编码,占用3个字节
由前面我们可以知道,String类中的getBytes方法可以使用平台的默认字符集将该 String编码为一系列字节,将结果存储到新的字节数组中**,其中IDEA中默认的编码是UTF-8,**因此用它来解码一个汉字,则会得到三个字节。
String类中的getBytes方法是一个重载的方法,具体如下:
Modifier and Type | 方法 | 描述 |
---|---|---|
byte[] |
getBytes() |
使用平台的默认字符集将该 String 编码为一系列字节,将结果存储到新的字节数组中。 |
byte[] |
getBytes(String charsetName) |
使用命名的字符集将该 String 编码为一系列字节,将结果存储到新的字节数组中。 |
因此我们可以使用第二个重载了的getBytes方法进行指定编码解码汉字
package Stream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
public class getBytesDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
String s = "中国";
byte[] bys01 = s.getBytes();
byte[] bys02 = s.getBytes("GBK");
System.out.println("UTF-8解码结果:" + Arrays.toString(bys01));
System.out.println("GBK解码结果:" + Arrays.toString(bys02));
/*
运行结果:
UTF-8解码结果:[-28, -72, -83, -27, -101, -67]
GBK解码结果:[-42, -48, -71, -6]
*/
}
}
但是我们使用字节流也可以复制文本文件,文本文件也会有中文,但是却没问题,这是为什么呢?
原因是最终底层操作会自动进行字节拼接成中文,但是是如何识别是中文的呢?
其实,汉字在存储的时候,无论选择哪种编码存储,其第一个字节都是负数,因此只要是字节是负数就说明它是汉字。
字符流涉及到编码方式,因此需要了解到编码表
基础知识
字符集
字符集是一个系统支持的所有字符的集合,包括国家文字,图形符号,数字等;
常见的字符集有:ASCII字符集、GBXXXX字符集、Unicode字符集
各个国家为了符合自己国家语言,然后设计出了一套属于自己国家的字符集,但是其他国家并没有它这套字符集,那么其他国家解码岂不是都是乱码???
其实并不然Unicode字符集的出现解决了这个现象,其中最为常用的就是UTF-8编码,互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码,它使用一至四个字节为每个字符编码。
必须强调的是采用哪种规则编码,就必须采用对应规则解码,否则会出现乱码!!!!
Writer:字节输出流的抽象类
而 OutputStreamWriter 是继承 Writer 的子类
其构造方法如下:
Constructor | 描述 |
---|---|
OutputStreamWriter(OutputStream out) |
创建一个使用默认字符编码的OutputStreamWriter。 |
OutputStreamWriter(OutputStream out, String charsetName) |
创建一个使用命名字符集的OutputStreamWriter。 |
常用方法如下:
Modifier and Type | 方法 | 描述 |
---|---|---|
void |
close() |
关闭流,先刷新。 |
void |
flush() |
刷新流。 |
void |
write(char[] cbuf, int off, int len) |
写入字符数组的一部分。 |
void |
write(int c) |
写一个字符 |
void |
write(String str, int off, int len) |
写一个字符串的一部分。 |
这些方法的使用方法大体上与字节流差不多,值得注意的是write方法传递的参数可以直接传递字符数组,不用先解码成byte数组再传递过去。
package Stream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
public class OutputStreamWriterDemo01 {
public static void main(String[] args) throws IOException {
//创建对象
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("IO\\osw.txt"));
//1.写入字符
osw.write('a');
//2.写入字符串
osw.write("\r\n中国\r\n");
osw.flush();
//3.字符数组
char[] ch = {'a', 'b', 'c'};
osw.write(ch);
osw.write(ch, 0, 1);
osw.flush();
//4.写入一部分
osw.write("HelloWorld", 0, 5);
osw.flush();
osw.close();
}
}
需要注意的是:OutputStreamWriter的中的close有点特别,它是在关闭之前会先刷新,也就是说在执行释放资源的之前,其会先主席那个flush方法,达到先把数据存储在文本信息的结果再释放资源。
Reader:字节输入流的抽象类
InputStreamReader 是继承了 Reader 的子类
其构造方法如下:
Constructor | 描述 |
---|---|
InputStreamReader(InputStream in) |
创建一个使用默认字符集的InputStreamReader。 |
InputStreamReader(InputStream in, String charsetName) |
创建一个使用命名字符集的InputStreamReader。 |
常用方法如下:
Modifier and Type | 方法 | 描述 |
---|---|---|
void |
close() |
关闭流并释放与之相关联的任何系统资源。 |
int |
read() |
读一个字符 |
int |
read(char[] cbuf, int offset, int length) |
将字符读入数组的一部分。 |
package Stream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
public class InputStreamReaderDemo01 {
public static void main(String[] args) throws IOException {
//创建对象
InputStreamReader isr = new InputStreamReader(new FileInputStream("IO\\osw.txt"));
//读一个字符数据数据
int ch;
while ((ch = isr.read()) != -1){
System.out.print((char) ch);
}
//一次读一个字符数组数据
char[] chs = new char[1024];
int len;
while ((len = isr.read(chs)) != -1){
System.out.println(new String(chs));
}
}
}
InputStreamReader的常用方法的使用大多与字节流类似
在IDEA中默认的编码解码都是使用UTF-8编码来实现的
但是如果我们构建字符输出流对象 OutputStreamWriter 的时候指定使用其他字符集编码,比如指定使用GBK编码,这是后我们打开已经写入数据的txt文件中,我们会发现出现了乱码情况,这是为什么呢???
这是因为txt默认使用UTF-8编码进行解码,但是编码是使用GBK进行编码的,因此肯定会出现乱码情况。
这时候我们读取数据显示在屏幕上的时候,如果我们创建字符输入流对象 InputStreamReader 的时候,没有指定使用GBK编码进行解码,那么它会默认使用UTF-8编码进行解码,进而出现乱码的情况,若我们需要显示出正常的内容而不是乱码,我们需要执行GBK编码进行解析。
总而言之,我们规定了使用哪种规则进行编码,就要使用哪种规则进行解析,否则会乱码!!!!
在处理现实问题中,比如复制一个java文件,如果其中不涉及编码问题,即不需要指定某种编码规则进行编码和解码,那么为了简便,我们可以使用 OutputStreamWriter 的子类 FileWrite 、 InputStreamReader 的子类 FileReader ;
但是如果一旦涉及到编码问题,还得使用 OutputStreamWriter 和 InputStreamReader 进行处理文件!!!
FileWrite 的构造方法如下:
Constructor | 描述 |
---|---|
FileReader(File file) |
创建一个新的 FileReader ,给出 File 读取。 |
FileReader(String fileName) |
创建一个新的 FileReader ,给定要读取的文件的名称。 |
由于 FileWrite 继承了 OutputStreamWriter ,所以 FileWrite 的常用方法的类型与用法与OutputStreamWriter一致。
FileReader 的构造方法如下:
Constructor | 描述 |
---|---|
FileWriter(File file) |
给一个File对象构造一个FileWriter对象。 |
FileWriter(File file, boolean append) |
给一个File对象构造一个FileWriter对象。 |
FileWriter(String fileName) |
构造一个给定文件名的FileWriter对象。 |
FileWriter(String fileName, boolean append) |
构造一个FileWriter对象,给出一个带有布尔值的文件名,表示是否附加写入的数据。 |
同样,由于 FileReader 继承了 InputStreamReader ,所以 FileReader 的常用方法的类型与用法与InputStreamReader 一致。
字符缓冲流主要是 BufferedWriter 与 BufferedReader, 两者分别继承两个抽象类 Writer 与 Reader ;
BufferedWriter :将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入,可以指定缓冲区大小,或者可以接受默认大小,默认足够打,可用于大多数用途。
BufferedReader:从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取,可以指定缓冲区大小,或者可以接受默认大小,默认足够打,可用于大多数用途。
BufferedWriter 字符缓冲输出流的构造方法如下:
Constructor | 描述 |
---|---|
BufferedWriter(Writer out) |
创建使用默认大小的输出缓冲区的缓冲字符输出流。 |
BufferedWriter(Writer out, int sz) |
创建一个新的缓冲字符输出流,使用给定大小的输出缓冲区。 |
从构造方法可知道,需要传递一个 Writer 类型的作为参数过去, 因此在使用字符缓冲输出流的时候,要先创建一个FileWriter对象。
BufferedWriter 字符缓冲输出流的常用方法如下:
Modifier and Type | 方法 | 描述 |
---|---|---|
void |
close() |
关闭流,先刷新。 |
void |
flush() |
刷新流。 |
void |
newLine() |
写一行行分隔符。 |
void |
write(char[] cbuf, int off, int len) |
写入字符数组的一部分。 |
void |
write(int c) |
写一个字符 |
void |
write(String s, int off, int len) |
写一个字符串的一部分。 |
这里面的很多方法都与前面的类似,但是这有一个特有的方法 newLine 方法, 其作用是写一行行分隔符,也就是换行的意思。
从上面我们可以知道,不同的操作系统的换行符号不一样,比如window的换行符号是\r\n,但是Linux的换行符号是\n,如果手动添加换行符,如果在不同的系统可能达不到换行的效果,因此,newLine 方法的出现,很好的解决了在不同系统换行的问题!!!
package Stream;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
public class BufferedWriterDemo {
public static void main(String[] args) throws IOException {
//创建字符缓冲流输出流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("IO\\bw.txt"));
bw.write("Hello");
bw.newLine();//换行
bw.write("World");
bw.close();
}
}
字符缓冲输入流 BufferedReader构造方法如下:
Constructor | 描述 |
---|---|
BufferedReader(Reader in) |
创建使用默认大小的输入缓冲区的缓冲字符输入流。 |
BufferedReader(Reader in, int sz) |
创建使用指定大小的输入缓冲区的缓冲字符输入流。 |
从构造方法可知道,需要传递一个 Reader类型的作为参数过去, 因此在使用字符缓冲输入流的时候,要先创建一个FileReader对象。
BufferedReader字符缓冲输入流的常用方法如下:
Modifier and Type | 方法 | 描述 |
---|---|---|
void |
close() |
关闭流并释放与之相关联的任何系统资源。 |
int |
read() |
读一个字符 |
int |
read(char[] cbuf, int off, int len) |
将字符读入数组的一部分。 |
String |
readLine() |
读一行文字。 |
这里面的很多方法都与前面的类似,但是其中有一个readLine的方法,可以一次读一行文字,然后返回一个String类型,这个方法十分实用
package Stream;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
//创建对象
BufferedReader br = new BufferedReader(new FileReader("IO\\bw.txt"));
String line;
while ((line = br.readLine()) != null){
System.out.println(line);
}
}
}
readLine方法可以读一行文字。 一行被视为由换行符(‘\ n’),回车符(‘\ r’)中的任意一个,紧跟换行符的回车符或通过到达文件末尾终止(EOF)。
一个包含行的内容的字符串,不包括任何行终止字符,如果没有读取任何字符,如果流的结尾已经到达,则为null ,因此可以使用这个特性作为循环的约束条件。