不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符,但是在我们日常的程序中操作的数据几乎都是字符,所以为了操作方便当然要提供一个可以直接写字符的 I/O 接口。而且从字符到字节必须经过编码转换,而这个编码又非常耗时,还经常出现乱码的问题,所以 I/O 的编码问题经常是让人头疼的问题,关于这个问题有一篇深度好文推荐一下:《深入分析 Java 中的中文编码问题》
下图是写字符的 I/O 操作接口涉及到的类,Writer 类提供了一个抽象方法 write(char cbuf[], int off, int len) 由子类去实现:
读字符的操作接口也有类似的类结构,如下图所示:
读字符的操作接口中也是 int read(char cbuf[], int off, int len),返回读到的 n 个字节数,不管是 Writer 还是 Reader 类它们都只定义了读取或写入的数据字符的方式,也就是怎么写或读,但是并没有规定数据要写到哪去,写到哪去就是我们后面要讨论的基于磁盘和网络的工作机制。
01.字节与字符的转化接口另外数据持久化或网络传输都是以字节进行的,所以必须要有字符到字节或字节到字符的转化。字符到字节需要转化,其中读的转化过程如下图所示:
InputStreamReader 类是字节到字符的转化桥梁,InputStream 到 Reader 的过程要指定编码字符集,否则将采用操作系统默认字符集,很可能会出现乱码问题。StreamDecoder 正是完成字节到字符的解码的实现类。也就是当你用如下方式读取一个文件时:
try { StringBuffer str = new StringBuffer(); char[] buf = new char[1024]; FileReader f = new FileReader("file"); while(f.read(buf)>0){ str.append(buf); } str.toString(); } catch (IOException e) {}FileReader 类就是按照上面的工作方式读取文件的,FileReader 是继承了 InputStreamReader 类,实际上是读取文件流,然后通过 StreamDecoder 解码成 char,只不过这里的解码字符集是默认字符集。
写入也是类似的过程如下图所示:
通过 OutputStreamWriter 类完成,字符到字节的编码过程,由 StreamEncoder 完成编码过程。
磁盘 I/O 的工作机制在介绍 Java 读取和写入磁盘文件之前,先来看看应用程序访问文件有哪几种方式;
几种访问文件的方式我们知道,读取和写入文件 I/O 操作都调用的是操作系统提供给我们的接口,因为磁盘设备是归操作系统管的,而只要是系统调用都可能存在内核空间地址和用户空间地址切换的问题,这是为了保证用户进程不能直接操作内核,保证内核的安全而设计的,现代的操作系统将虚拟空间划分成了内核空间和用户空间两部分并实现了隔离,但是这样虽然保证了内核程序运行的安全性,但是也必然存在数据可能需要从内核空间向用户用户空间复制的问题;
如果遇到非常耗时的操作,如磁盘 I/O,数据从磁盘复制到内核空间,然后又从内核空间复制到用户空间,将会非常耗时,这时操作系统为了加速 I/O 访问,在内核空间使用缓存机制,也就是将从磁盘读取的文件按照一定的组织方式进行缓存,入股用户程序访问的是同一段磁盘地址的空间数据,那么操作系统将从内核缓存中直接取出返回给用户程序,这样就可以减少 I/O 的响应时间;
00. 标准访问文件的方式读取的方式是,当应用程序调用read()接口时:
①操作系统首先检查在内核的高速缓存中是否存在需要的数据,如果有,那么直接从缓存中返回;
②如果没有,则从磁盘中读取,然后缓存在操作系统的缓存中;
写入的方式是,当应用程序调用write()接口时:
从用户地址空间复制到内核地址空间的缓存中,这时对用户程序来说写操作就已经完成了,至于什么时候在写到磁盘中由操作系统决定,除非显示地调用了 sync 同步命令;
01.直接 I/O 方式