之前稍微提了下java.io包下的File类,今天我们就深入来看下java.io包下常用的类,也就是IO流了。
IO流很多,都掌握是不太可能的,这里作者列出了接下来要看的IO流。那么什么是IO流呢?I看成是Input,O看成是Output。解释起来就是输入和输出。可以这么理解,在你的桌面上有一个已经写好的.java文件,你想用java读取里面的内容这个过程就是输入/I了,而想用java输出一些数据到这个.java文件里并保存,这个过程就是输出/O了。所以我们可以知道了IO流的主要作用就是与本地文件进行交互。(读者千万不要以文件本身为参照物,这样看的话会感觉读取文件内容像是输出,而写入文件内容像是输入;这里的输入输出是以java为参照物的)
IO流的分类
目前的分类主要有三种:
1、按照流的方向分:输出流和输入流。其中InputStream和Reader都是输入流。而OutputStream和Writer都是输出流。
2、按照流的数据单位分:字节流和字符流。其中InputStream和OutputStream都是字节流。而Reader和Writer都是字符流。字符流,就是按照一个字符一个字符的顺序输入或输出,所以字符流只适用于纯文本的文件,像图片、音频等的文件就无法进行输入或输出。而字节流就没有这种范围限制了,因为我们的计算机存储文件都是用字节来计算的,所以字节流所有的本地文件都能读取。
3、按照流的功能分:节点流和包装流(或者称处理流):简单理解,构造方法没有传入流的都是节点流,而构造方法传入流的都是包装流。等下详细讲解流的时候就能够更清晰的知道这个概念了。
两大接口:Closeable与Flushable对于IO流中最主要的两大接口,我们需要知道这两者存在的意义:
查看源码其实很简单,Closeable就只有一个close方法让子类实现,Flushable就只有一个flush方法让子类实现。但仔细看源代码的读者会发现,所有的流都实现了Closeable接口,意思是可关闭的。所有的流用完之后一定要关闭!而所有的输出流都实现了Flushable接口,意思为可刷新的,所有的输出流用完之后一定要先刷新后关闭。刷新的主要作用就是把未输出的数据强行输出,如果没有刷新的话很容易造成数据丢失!而如果一个流使用完之后不关闭的话虽然表面上看起来没事,但是这么做会占用额外的内存空间,甚至有可能导致内存溢出。
四大抽象类:InputStream、OutputStream、Reader、Writer作为IO流最主要的四大抽象类,我们需要知道下四者的大致中文意思:InputStream字节输入流,OutputStream字节输出流,Reader字符输入流,Writer字符输出流。而对于这四大抽象类,我们分别看看里面的一些常用方法:
InputStream:
估算里面可读取的字节数。
关闭流。
输入流必须掌握的方法。(等下在具体的实现类中示范如何使用),因为是字节输入流,所以可以传入byte数组。
跳过指定的字节数。
OutputStream:
前两个是对应关闭流和刷新流的方法,而后两个是输出流必须掌握的方法。(等下在具体的实现类中示范如何使用)
Reader:
关闭流。
输入流必须掌握的方法。(等下在具体的实现类中示范如何使用),因为是字符输入流,所以可以传入char数组。
跳过指定的字符数。
Writer:
和OutputStream类似,就不细说了。唯一的区别是可以输出字符串:
接下来就开始看具体的实现类了,而对于要讲的实现类,又可以作以下的划分:
1、文件专属流:FileInputStream,FileOutputStream,
FileReader,FileWriter
2、转换流:
InputStreamReader,OutputStreamWriter
3、缓冲流:
BufferedInputStream,BufferedOutputStream,
BufferedReader,BufferedWriter
4、数据流:
DataInputStream,DataOutputStream
5、标准输出流:
PrintStream
6、对象专属流:
ObjectInputStream,ObjectOutputStream
文件专属流对于四个文件专属流,常用方法都在对应的抽象类中了,接下来我们就看构造方法和常用方法的应用:
FileInputStream常用的就是第三个或第一个。可以传入一个File类的对象或者路径名。知道了怎么构造后我们来看看它的方法是怎么运用的。在这之前我们在idea开发工具的Test模块下建一个Message的文件:
之后的输入流就从这个文件输入了。而这个文件的内容是:
在这里插入下IO流使用的一些细节:
因为IO流要与本地文件进行交互,所以难免会有文件找不到等异常的出现,这时候需要try...catch进行异常处理:
之后我们用完流后需要关闭,close方法也有抛出异常所以也要try...catch进行处理:
于是一个简单的IO流就变成了这样,但这样有一个很大的问题:在我们将文件内容输入到java或之后书写的代码中可能也会抛异常,因而导致之后关闭流的代码没有执行,比如:
我们在使用FileInputStream的read方法时有可能会抛出IOException,当捕获到IOExcepion后就会跳转到catch语句,之后的close方法并没有执行。这无疑是个bug,那么怎么修改这个bug呢?因为我们流的关闭是放在最后的,所以我们可以在try...catch中加一个finally语句:但因为finally和try是两个语句块,所以我们还要把FileInputStream对象提取出来才能在try和finally语句块中进行操作,于是乎,代码可以变成如下:
当然,当我们的流对象是null的时候可以认为流并没有开启,所以finally的语句块中又可以稍微改善:
这时候一个IO流书写的模板就大致出来了。(以上书写文件路径使用的是相对路径,而idea开发工具中路径相对的是当前工程的,上图中我的工程中有JAVA SE和Test两个模块,Test模块中有我们需要的Message文件,所以写相对路径就是Test/Message)
知道了流的大致书写方式,我们就通过简单的例子来知道常用方法的使用:
从FileInputStream的源代码分析可以知道这个方法每次读一个字节,并返回读取字节的ASCII码值;如果一直调用直到读不到字节了,返回-1;
这里为什么还要指定一个中间变量data呢?原因和我们集合的iterator的next方法类似,这里每read一下就会往后移一个字节。所以需要用中间变量来接收。运行之后可以看到控制台输出了一系列ASCII码值:
中文的ASCII码值不看了,看前面四个英文字母,是不是一一对应着自己的ASCII码值。而如果没有中间变量,就会缺失很多数据:
上面的read一个一个字节读取,效率并不高,于是我们就可以在read方法中传入一个byte数组,这时候读取byte数组的长度个字节,大大提高了效率,并且读取的内容存到了byte数组里,返回了读取的字节数:
运行结果:
如果不想看对应的ACSII码值,我们可以通过String的构造方法传入byte数组使之变成String:
这个read方法和上面的方法差不多,唯一的区别是后面两个参数指定了读入到byte数组的起始位置和读取的长度:
可以看到跳过了4个字节,前面的AaBb没有读到。
最后一个方法了,这个方法返回流中估计的可读取的字节数,那么这个方法有什么用呢?我们可以直接通过这个方法建一个一次性读取全部字节的数组:
不过这个方法不适用于很大的文件,因为很大的文件好几个字节,而数组我们之前就提到过长度不能太长。
FileOutputStream有五个构造方法,第三个不看,就看剩下的四个,会发现基本的构造方法也是传入路径名或File类的对象。那么这两种构造方法可以跟一个boolean类型的参数起着什么作用呢?等下就知道了。
接下来看看常用方法:(close和FileInputStream一样,flush方法也是写在最后面,有抛出IOException)
第三个方法知道就行,我们可以指定字节,一次一次输出。接下来看第一个write方法(第二个write方法和上面的read的第三个方法类似,就不演示了):
这里的构造方法没有加boolean参数,我们运行后看看Message的文件:
会发现,之前文件的内容都不见了,但如果我们在构造方法里面加boolean参数true的话:
就会从原有的内容继续写:
因此我们可以知道FileOutputStream的构造方法加入一个boolean类型的参数表示是否从文末继续追加写入。而且从以上代码可以看到上述的方法没有像输入流那样抛出文件没有找到异常,那么如果构造方法传入的文件不存在的话会怎么样呢?
实践证明如果不存在指定文件,那么输出流会自己创建对应的文件。
FileReader与FileWriter为什么要将两者放一起呢?我们看看FileReader和FileWriter的构造方法和常用方法:
构造方法和上述的FileInputStream、FileOutputStream一样,而且从上面四大抽象类方法可以知道FileReader和FileWriter的方法使用和FileInputStream、FileOutputStream差不多,唯一区别就是byte数组变成char数组。而且FileWriter可以直接传入字符串写入,不用拆分成byte数组。
转换流对于InputStreamReader和OutputStreamWriter,可以通过其构造方法来完成相应的字节流到字符流的转换:
可以看出构造方法传入对应的字节流就可以转换为字符流。
缓冲流BufferedInputStream与BufferedOutputStream对于这两个缓冲字节流,我们看API文档:
可以知道缓冲流的存在就是为了提高读写效率的。
可以看到这里的构造方法和转换流一样也是得传入流的,不能直接传一个路径名或者File类的对象。
BufferedReader与BufferedWriter和文件专属流一样,BufferedReader和BufferedWriter和BufferedInputStream、BufferedOutputStream类似:
值得注意的是BufferedReader有一个可以读取一个文本行的方法:
可以看到这个方法并没有读取换行符,输出的时候需要我们手动换行;当没有读到有效字符后返回null。
而BufferedWriter有一个写入换行符的方法:
可以发现缓冲流中的这个方法可以使我们更好的输出。
标准输出流初学Java我们就是学System.out.println的这个语句,其实这个System.out就是一个PrintStream类:
所以现在读者可以知道我们平常输出到控制台的print和println方法其实都是PrintStream的常用方法了。在这里就不对PrintStream多做解释,来看一个System里面比较好玩的东西:
对于System这个类,里面有三个成员变量:err,in,out,并且我们可以设置这三个变量的输入或输出路径,这里用setOut举例:
可以发现这时候系统的System.out.println/print都不是输出到控制台了,而是输出到Message文件中。
数据流DataOutputStream可以看到这个数据输出流可以通过相应的方法来存储八大基本数据类型:
从右边的执行结果可以知道写入之后并不是纯文本。
DataInputStream与上面的DataOutpurStream相对应,DataInputStream也提供了相应的八个方法来读取数据:
注意:当使用DataInputStream读取之前由DataOutputStream写入的文件时,之前写入的是什么顺序,读取这些数据时也要用什么样的顺序。否则数据会发生错误:
对象专属流八大基本数据类型都能够读取了,引用数据类型当然也得有:
ObjectOutputStream与ObjectInputStream输出对象常用方法:
输入对象常用方法:
会发现也是输出了非纯文本的形式。但如果我们想写入自己的类对象:
会发现并没有之前那么简单了,会报NotSerializableException异常。为什么之前java输出自带的类就没有报异常呢?仔细看源代码就会发现它们其实都实现了Serializable接口,译为可序列化的。于是乎,就有了序列化和反序列化的概念:将Java对象放入到本地文件中的过程叫做序列化;将Java对象从本地文件取出的过程叫反序列化。如果要序列化对象,就必须要实现序列化(Serializable)接口。那么这个接口有什么用呢?
看源代码会发现里面并没有什么东西,但是这个接口起着非常重要的标记作用,它告诉JVM这个类的对象是可以序列化的,并且会自动生成一个序列化的版本号。那么序列化版本号有什么作用呢?可以这么理解:它是第二个区分类的方法。而自动化生成版本号的特点是版本号一旦生成之后,如果改原来的代码就会出错,因为修改了代码就会重新编译,重新编译JVM就会重新生成一个序列化版本号(比如你之前序列化类对象后,修改了这个类的代码,那么后期反序列化的时候就会报InvalidClassException异常),那么如何解决这个问题呢?我们可以通过手写固定序列化版本号,那么就可以解决了:
我们可以根据源代码手写的格式来模仿:
这时候序列化Dog类对象后即使修改Dog类的源代码,反序列化也不会出错了。
那么如果我们的类对象中有的属性不希望它序列化呢?这时候我们可以在属性中加修饰符trasient,译为游离的,这样在序列化的时候,这个属性就不会序列化进去了(这里的属性针对于实例变量,静态变量因为是类级的,所以本身不参与序列化)。
可以看到Dog的name属性被trasient修饰后,序列化后又反序列化并没有读取到原来Dog对象的name。
当然,以上的写入对象的方式是有点麻烦的,我们可以将多个对象放入到集合里面,序列化集合,之后反序列化后再从集合取出对象。(反序列化集合后需要强制转换为集合)
到这里,基本的IO流就讲完了,相信读者应该知道了以上的流哪些是节点流,哪些是包装流了:
节点流:文件专属流
包装流:转换流,缓冲流,数据流,标准输出流,对象专属流
而且从上面例子我们可以看到包装流用完之后关闭就行,构造方法传入的流不用关闭。因为看源代码可以发生包装流的close方法已经把构造方法传入的流关闭了。
集合Properties与IO流对于集合中的Properties,那时候我们大致了解了它的作用,今天我们看看它里面的几个与IO流有关的方法:
我们可以通过以上的方法来将属性输出到指定流,或者从指定流输入到属性。这里我们看load方法,list方法读者可以自行实验,这里不演示了。
那么用load方法传入的输入流里的文件内容需要有什么样的格式呢?大致上如下:
因为Properties里存的是键值,所以我们就用=(或者:不过很少用)来隔离键值,前面表示键,后面表示值,这种格式的文件一般称为属性配置文件。#表示注释。
属性配置文件一般都以.properties结尾。
资源绑定器对于读取属性配置文件,还有一种更好的方法,用java.util包下的抽象类ResourceBundle。这个类专门来获取属性配置文件的,但有两点一定是要遵守的:这个属性配置文件必须放在类路径下,也就是src下(因为它是从类路径下开始找的);这个属性配置文件必须要有扩展名.properties。
我们可以通过它的这个静态方法来获取资源绑定器对象,然后通过以下几种方法来获取key和value:
还需要注意的是,构造方法里传入属性配置文件的名称而已,不能加扩展名!!!以上IO流例子中没有加文件扩展名是因为文件本身没有扩展名,如果加了扩展名:
可以看到这时候Message有了扩展名.txt,但我们写IO流的时候没有加扩展名,所以报错了。