JAVA I/O机制(一)

Posted by ShiYu on 2017-11-21

Java的I/O操作类 主要分为以下4组

  • 基于字节操作的I/O接口:InputStream和OutputStream
  • 基于字符操作的I/O接口:Writer和Reader
  • 基于磁盘操作的I/O接口:File
  • 基于网络操作的I/O接口:Socket

I/O是人与机器或机器与机器交换信息的手段,核心问题就是要将什么样的数据写到什么地方,前两组描述的是传输数据的数据格式,即“什么样的数据”,后两组是传输数据的方式,即传到什么地方。

基于字节的I/O操作接口

基于字节的输入接口是InputStream,InputStream的类层次结构如下图所示:

基于字节的操作接口类图

基于字节的输出接口是OutputStream,OutputStream的类层次结构如下图所示:

基于字节的输出接口类图

基于字符的I/O的输入接口是Reader,它的类层次结构如下图:

基于字符的输入接口类图

基于字符的I/O输出接口是Writer,它的类层次结构如下图:

基于字符的输出接口类图

字节与字符的转化

无论是磁盘还是网络传输,最小的存储单元都是字节而不是字符,I/O操作的都是字节而非字符,之所以有操作字符的I/O接口,原因是我们程序中经常操作的数据是字符形式,为了操作方便而封装了直接操作字符的接口,从字符到字节必须经过编码转换,如果字符到字节decode所用的编码和字节到字符encode所用的编码不一致,就会出现乱码问题。

数据持久化或网络传输都是以字节进行的,所以必须要有从字符到字节的转化,下图描述的是字符到字节的转化过程:

字符到字节转化图

数据持久化和网络传输是以字节进行的,而人类识别的是字符,所以必须要有字节到字符的转换,下图是字节到字符的转化过程:

字节到字符转化图

Java访问磁盘文件

以利用FileReader访问文件为例,整个访问流程如下图:

java访问磁盘文件

文件存储在磁盘中是以字节形式存储的,FileReader最终读取到的是字符,中间经历了上图所示的转换,首先会创建一个File对象,这个File对象并不代表一个真实存在的文件对象,当你指定一个路径描述符时,它只是一个代表这个路径的虚拟对象,只有在真正需要读取这个文件时才会创建一个真正代表这个文件的对象:FileDescriptor,由上图可知,在创建FileInputStream对象时,会创建一个FileDescriptor对象,这个对象就是一个真正代表一个存在文件的对象的描述,当我们在操作一个文件对象时可以通过getFD()方法获取真正操作的与底层操作系统相关联的文件描述。例如可以调用FileDescriptor.sync()方法将操作系统缓存中的数据强制刷新到物理磁盘中。

下面解释一下上面的从磁盘读取文件的流程图,FileReader继承自InputStreamReader,首先FileReader的构造方法会创建一个FileInputStream对象当做基类InputStreamReader构造方法的参数,在InputStreamReader构造方法中,会实例化一个StreamDecoder解码对象,解码对象实例化时采用的编码以用户自定义的编码为准,缺省值采用ISO-885591编码,当调用read()方法时,会通过FileInputStream读取到存储在磁盘中的字节,然后通过StreamDecoder解码成字符返回。

从上面可知,读取字符实际上是先读取字节然后通过StreamDecoder解码成字符的,所以在读取字符时必须指定与序列化时采用的编码一样的编码,否则就会出现乱码,由于默认编码是ISO-88591,而中文编码一般采用gbk或者utf8,所以在读取中文文件时如果不指定正确的编码,通常都会出现乱码,这就是乱码产生的根本原因。

Java序列化

Java序列化就是将一个对象转换成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。需要持久化的对象必须继承java.io.serializable接口。反序列化则是将持久化的字节数组再重新构造成对象。

Java序列化通过运行时校验类的serialVersionUID来验证版本一致性,即序列化的class与本地的class是否一致,如果实现serializable接口时没有显式地声明serialVersionUID,Java序列化机制会在编译class时自动生成一个UID用于序列化版本校验,这种情况下只有同一次编译的class才会生成相同的UID,也就是说只有对于序列化的数据,只有使用同一次编译的class才能反序列化成功。正常情况下我们将UID显式地声明为1L就可以满足使用。

下面是一些对序列化的总结:

  1. 当父类实现Serializable接口时,所有子类都可以序列化
  2. 子类实现了Serializable接口,父类没有,父类中的属性不能序列化,但是子类中属性仍然可以正确序列化,父类中的属性就相当于用transient关键字修饰一样
  3. 如果序列化的属性是对象,那么这个对象也必须实现Serializable接口,否则会报错
  4. 向同一个文件多次序列化同一个对象,在反序列化后得到的也是同一个对象,且状态是第一次写入时候的状态,这是因为
  5. 在反序列化时,如果serialVersionUID被修改,则反序列化会失败