java中对于输入输出相关操作进行了抽象,统称为java的IO流操作。这里,我们在以往java文件读写操作(
FileWriter
和FileReader
)基础上,进一步深入学习java的IO流概念。
IO流分类
根据不同的分类标准,我们可以对IO流进行不同的的划分。现暂以IO流向、IO操作数据类型和IO流功能这三个基准,来对IO流进行大致分类,如下:
按流向分
名称 | 作用 | 举例 |
---|---|---|
输入流 | 读取数据 | FileReader |
输出流 | 写出数据 | FileWriter |
按数据类型分
名称 | 作用 | 举例 |
---|---|---|
字节流 | 以byte为单位读写 | InputStream OutputStream |
字符流 | 以char为单位读写 | FileReader FileWriter |
按功能分(是否直接操作数据源)
名称 | 说明 | 举例 |
---|---|---|
节点流 | 可以直接从数据源或目的地读写数据 | FileReader FileInputStream |
包装流 | 不直接连接数据源或目的地,是其他流的封装, 目的是简化操作或提高效率 | InputStreamReader PrintWriter |
IO流读写操作
按照上面的IO流分类,我们也可以将IO流操作分为字节流操作和字符流操作,下面将分别进行介绍:
字节流读写 – 二进制文件搬运工
计算机文件,本质上就是对一连串“0101XXX”等二进制数据的存储。正因为如此,一切文件(不管是视频、音频、图片,还是普通文本文件)都可以通过对底层二进制数据的读写,以实现特定的复制、修改操作。
这里以图片的复制操作为例,相关代码如下:
1 | //1.创建FileInputStream对象和FileOutputStream对象 |
字符流读写 – 文本文件好帮手
特别的,除了像上面那样通过字节流完成读写外,文本文件还可以通过字符流的方式完成读写操作。
这是因为文本文件的二进制存储,都是以特定的的编码方式实现的(例如unicode编码);这意味着当这些二进制数据被读入内存,并存入char型数组时,计算机可以正确地完成相应的编解码工作,不会发生数据丢失的状况。
而普通的二进制文件(比如一张图片),由于其二进制存储并未遵循字符编码规则;一旦其被读入内存,并存入字符数组时,计算机会依照相应的编码规则进行解码操作;这时候就会发生有些二进制bit被误当作编码的标志位而丢弃,造成了数据的损坏。
下面我们对字符的编解码进行相关总结:
编码表
可以简单的理解为:一张字符与数值的映射表。根据这张表,计算机可以将内存中存储的数值所对应的字符进行渲染显示。
最早的编码表是ASCII
,其只覆盖了阿拉伯数字、英文字母和一些常用符号,主要供英语使用;随后各国根据本国语言,相继推出了本国的编码表:例如中国的GBK
。
由于各国编码表不一致,常常出现文件移动到不同编码的计算机后出现乱码现象,于是一种兼容所有编码规则的编码表Unicode
出现了。最先推出Unicode
编码时,是将2个字节作为每个字符的存储单元,但很快就不够用了;于是乎推出了用4个字节存储每个字符的规则,这种方案虽然解决了存储单元空间不足的问题,但也同时造成了存储空间的大量浪费(比如用4个字节去存储只占一个字节的字符)。
在这样背景下,一种可变单元存储空间的编码规则UTF-8
出现了,它本质上还是Unicode
,只不过改进了前者的存储规则。
另外我们还要提一下ANSI
编码,它又叫本地编码表;和上面提到的编码不同,它并不是一种具体的编码规则,而是本地编码的代指。如果你的本地编码是GBK
,那么ANSI
就是GBK
;如果你的本地编码是UTF-8
,那么ANSI
就是UTF-8
。
字符串编码
要保证字符串写入文件或打印到console时不乱码,基本思路是:编解码前后一致,包括字符集与字节长度
1 | //以GBK编码写入数据到文件 |
字符流编码
字符流 = 字节流 + 编码
1 | OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("b.txt"),"GBK"); |
相关文本文件操作详情可以参考之前的总结java – 文本文件读写
相关unicode等字符编码可以参考字符编码笔记:ASCII,Unicode 和 UTF-8
常用IO流类介绍
标准输入输出流
有关标准输入输出流,我们在之前的日常编码中经常用到,只不过没有引起我们的关注罢了。
诸如向控制台打印System.out.println("xxxxx")
或者是键盘录入new Scanner(System.in)
,我们都可以发现Systemm
类的身影。事实上,System
中的类字段out
以及in
就是我们要谈到的标准输出输入流对象。
1 | /** |
相关JDK源码如上,不管是in
还是out
都是字节流对象;其中in
用于读取键盘录入数据,而out
用于向屏幕输出数据。
OutputStreamWriter && InputStreamReader
由于标准输入输出流处理的都是字节流数据,而我们人类可以识别的只能是字符流数据;一旦我们打算将所谓的字符流数据打印到屏幕,或者是把键盘输入的字节流数据保存为文本文件时,都会不可避免地遇到数据流间相互转化的问题。
例如,我们打算将文本文件的字符流数据打印到console,可以采用如下的办法:
1 | //read file |
以上方法的不便之处显而易见,这种手动的转换极容易被忽视,进而导致错误。
从这一痛点出发,java为我们引入了OutputStreamWriter
类,帮助我们自动完成字符流向字节流的转换。使用OutputStreamWriter
重写以上方法如下:
1 | //read file |
和将文本文件的字符流数据打印到console中类似,我们从键盘录入字节流数据并将其存储在文本文件当中,可以使用InputStreamReader
完成字节流向字符流的自动转换。相关代码如下:
使用手动转换
1 | FileWriter fw = new FileWriter("a.txt"); |
使用InputStreamReader
自动转换
1 | BufferedWriter bw = new BufferedWriter(new FileWriter("a.txt")); |
打印流
打印流大体可以分为两类:字符打印流PrintWriter
和字节打印流PrintStream
。和其他IO流不同的是,打印流仅仅存在输出流类,没有对应的输入流类。
由于PrintStream
就是我们之前一直谈到到的System.out
,这里不再赘述。
事实上,PrintWriter
已经完全可以取代PrintStream
的角色,两者的区别就在于:
自动flush,
PrintStream
一般可以自动刷新,而PrintWriter
只有特定方法(如print())才有PrintStream
不能包装一个Writer
,即无法读入字符流
之所以现在还存在PrintStream
,是因为其出现要早于JDK1.1,并且JDK中大量使用了它,例如System.out;如果弃用PrintSteam
会给JDK带来巨大的重构麻烦。
构造方法
至于PrintWriter
,它是一个字符流输出类。这里我们一起看一下它的构造函数:
1 | public PrintWriter(File file) throws FileNotFoundException { |
PrintWriter
基本上都是有参构造函数,传入的参数有File
对象、Writer
字符输出流对象、以及OutputStream
字节输出流对象。其中传入的File
对象通过调用new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))
,最终转为了Writer
对象;而OutputStream
字节输出流对象,通过new BufferedWriter(new OutputStreamWriter(out))
也最终转为了Writer
对象。
这里我们可以稍微总结一下:PrintWriter
由于自身并不具备字符输出功能,因此是一个包装流,它内部通过调用基本流Writer
对象完成字符流IO操作。
特有成员函数
这里我们主要介绍一下PrintWriter
有关自动换行和自动刷新的方法。相关代码如下:
1 | PrintWriter pw = new PrintWriter(new FileWriter("a.txt"),true); |
如上所示,我们可以通过构造函数中的第二个boolean参数手动开启自动刷新功能。另外,同System.out.println()
类似,PrintWriter
同样可以使用println()
实现输出换行的目的。
注意:即使手动开启了自动刷新,也并不是说PrintWriter
中的所有方法调用时都有效果。JDK特别指出,只有当调用print()
、println()
以及format()
方法时,才存在自动刷新效果
利用PrintWriter进行文件复制
1 | PrintWriter pw = new PrintWriter(new FileWriter("dest.txt"),true); |
数据操作流
和InputStreamReader、OutputStreamWriter比较类似,都是非字节数组的数据与字节数组之间的转换。不同的是 DataInputStream、DataOutputStream 能够处理的范围更广。
例如:InputStreamReader 只能将输入字节流转化为字符串;而DataInputStream 可以将字节流按输入顺序,依次转为int、boolean、String、char类型数据。
下面分别从各种数据类型与二进制文件的相互转换以及各种数据类型与字节数组的相互转换两方面来加以介绍:
1 | /** |
1 | /** |
对象操作流
顾名思义,这是一个用于读写对象的IO流。按照惯例,我们同样可以将其分为两类:对象输入流ObjectInputStream
以及对象输出流ObjectOutputStream
下面对这两个类进行详细介绍:
类名 | 构造方法 | 常用成员方法 |
---|---|---|
ObjectOutputStream | ObjectOutputStream(OutputStream out) | void writeObject(Object obj) |
ObjectInputStream | ObjectInputStream(InputStream in) | Object readObject() |
注意事项
需要指出的是,我们在使用对象操作流进行对象读写时,需要注意对象所在类是否实现了Serializable
接口,不然会报java.io.NotSerializableException
。
除此之外,我们在对象类中还需手动指定序列化IDserialVersionUID
,以防止读取已经保存的对象流文件时,由于对象所属类的修改,产生的文件中保存的序列化ID和类中自动计算的序列化ID不一致的异常。这种情况下,通常会报java.io.InvalidClassException xxxxxx local class incompatible
等异常。
比较稳妥的写法如下:
1 | public class ObjectOutputStream{ |
最后还需注意的是:我们在上面代码中用对象操作流输出了两个对象到文件中,但当我们需要从文件中读取这些对象时,很自然的会遇到需要确定读取对象个数的问题。比如下面代码:
1 | ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt")); |
在执行到Object o3 = ois.readObject()
,会抛出异常java.io.EOFException
,原因是此时文件已经读到尾了。和之前读取文件中的字符或字节流不同的是,之前一旦读到文件末尾我们可以通过返回-1
的方式获知情况并终止读取操作;而在读取对象输出流文件中却没有这种机制,我们只能通过捕获异常手动处理方式来解决。
事实上,像这种读取多对象输出流文件的问题,我们可以考虑用一个集合存储多个对象,再将集合以对象流的形式写入文件;这样一来,在读取文件时就不用关系读取对象个数的问题了。本着这个思想,将上述代码重构如下:
1 | ObjectOutputStream oos = new ObjectOutputStream((new FileOutputStream("b.txt"))); |
Properties – 配置文件读写能手
一种特殊的双列集合,实现了Map
接口,继承了Hashtable
。不同于HashMap
,Properties
中的键值对都为String
类型。
另外,由于Properties
实现了属性集的持久化,故又称之为属性列表。
构造方法
1 | /** |
如上,Properties
提供了两个构造方法;空参的构造方法用于实例化一个空的属性列表,有参的构造方法可以生成一个指定默认值的属性列表。
常用成员方法
由于Properties
实现了Map
接口,理论上Map
的方法其都可以调用。例如增加属性,我们可以使用put(key,value)
;删除属性,则可以使用remove(key)
。
但Properties
类的官方注释并不推荐我们这么做,理由是使用Map
接口的put(key,value)
方法可以增加非字符串的属性行,这将导致Properties
中store
hesava
方法的调用失败。
这里我们将Properties
中的常用方法总结如下:
方法名 | 描述 |
---|---|
Object setProperty(String key, String value) | 新增Properties属性行 |
getProperty(String key) | 查询指定key的属性值 |
String getProperty(String key, String defaultValue) | 查询指定key的属性值,没有则返回默认值 |
另外,日常工作中我们会经常使用Properties
来进行属性文件的读写,因此特将Properties
的IO操作总结如下:
使用Properties
中的void list(PrintWriter out)
或void store(Writer writer, String comments)
将属性写入文本文件:
使用list()
1 | Properties prop = new Properties(); |
使用store()
1 | Properties prop = new Properties(); |
使用Properties
中的void load(Reader reader)
从文件中读取属性:
1 | Properties prop = new Properties(); |