Java I/O类库的基本架构

Java 的 I/O 操作类在包 java.io 下,大概有将近 80 个类,但是这些类大概可以分成四组,分别是:

基于字节操作的 I/O 接口:InputStream 和 OutputStream

基于字符操作的 I/O 接口:Writer 和 Reader

基于磁盘操作的 I/O 接口:File

基于网络操作的 I/O 接口:Socket

前两者主要根据传输数据的数据格式进行划分,后两组主要是根据传输数据的传输方式进行划分。

(注意:这里的Socket其实是不在java.io包下的)。

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

基于字节的 I/O 操作接口输入和输出分别是:InputStream 和 OutputStream。

InputStream 输入流的类继承层次:

OutputStream 输出流的类继承层次:

基于字符的 I/O 操作接口:

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符,但

是为啥有操作字符的 I/O 接口呢?这是因为我们的程序中通常操作的数据都是以字符形式,为了操作方便当然

要提供一个直接写字符的 I/O 接口,如此而已。我们知道字符到字节必须要经过编码转换,而这个编码又非常

耗时,而且还会经常出现乱码问题,所以 I/O 的编码问题经常是让人头疼的问题。

Writer 相关类层次结构:

Reader 相关类层次结构:

字节与字符的转化接口

另外数据持久化或网络传输都是以字节进行的,所以必须要有字符到字节或字节到字符的转化。字符到字节需要转化。

字符解码相关类结构:

InputStreamReader 类是字节到字符的转化桥梁,InputStream 到 Reader 的过程要指定编码字符集,否则

将采用操作系统默认字符集,很可能会出现乱码问题。StreamDecoder 正是完成字节到字符的解码的实现类。

StringBuffer str=new StringBuffer();
char[] buf=new char[1024];
FileReader f=new FileReader("file");
while(f.read(buf)>0){
str.append(buf);
}
str.toString();
//FileReader类就是按照上面的工作方式读取文件的,
//FileReader是继承了InputStreamReader类,
//实际上是读取文件流,然后通过StreamDecoder解码成char,只不过这里的解码字符集是默认字符集

写入也是类似的过程:

Java读写磁盘

读取和写入文件I/O操作都调用操作系统的接口,

因为磁盘设备是由操作系统管理的,

应用程序要访问物理设备只能通过系统调用的方式来工作。

读和写分别对应read()和write()两个系统调用。

而只要是系统调用就可能存在内核空间地址和用户空间地址

切换的问题,这是操作系统为了保护系统本身的运行安全,

而将内核程序运行使用的内存地址和用户程序运行的内存空间进行隔离造成的。

但是这样虽然保证了内核程序运行的安全性,

但是也必然存在数据可能需要从内核空间向用户空间复制的问题。

如果遇到非常耗时的操作,如磁盘I/O,

数据从磁盘复制到内核空间,然后又从内核空间复制到用户空间,将会非常缓慢。

这时操作系统为了加速I/O访问,在内核空间使用缓存机制,

也就是将磁盘读取的文件按照一定的组织方式进行缓存,

如果用户程序访问的是同一段磁盘地址的空间数据,

那么操作系统将从内核缓存中直接去除返回给用户程序,

这样可以减少I/O的响应时间。

5种访问文件的方式:

1.标准访问文件的方式

标准访问文件的方式就是当应用程序调用read()接口时,

操作系统检查在内核的高速缓存中有没有需要的数据,

如果已经缓存了,那么就直接从缓存中返回了,

如果没有,则从磁盘中读取,然后缓存在操作系统中缓存。

写入的方式是,用户的应用程序调用write()接口将数据

从用户地址空间复制到内核地址空间的缓存中。

这时对用户程序来说写操作就已经完成,

至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync同步命令。

标准访问文件的格式:

2.直接I/O的方式

应用程序直接访问磁盘数据,而不是经过操作系统内核数据缓冲区,

这样做的目的减少一次从内核缓存区到用户程序缓存的数据复制,

这种访问文件的方式通常是对数据的缓存管理由应用程序实现的数据库管理系统中。

如在数据库管理系统中,系统明确地知道应该缓存哪些数据,应该失效哪些数据,还可以对

一些热点数据做预加载,提前将热点数据加载到内存,可以加速数据的访问效率。

在这些情况下,如果由操作系统进行缓存,则很难做到,因为操作系统并不知道哪些是热点数据,

哪些可能只会访问一次就不会再访问,操作系统只是简单的缓存最近一次从磁盘读取的数据。

但是直接I/O也有负面影响,如果访问的数据不在应用程序中缓存,

那么每次从磁盘进行加载,而这种直接加载会非常缓慢。

通常直接I/O与异步I/O结合使用,会得到比较好的性能。

直接I/O的方式:

3.同步访问文件的方式

数据的读取和写入都是同步操作,它与标准访问文件的方式不同的是,

只有当数据被成功写到磁盘时才返回给应用程序成功的标志。

这种访问文件的方式性能比较差,只有一些对数据安全性要求比较高

的场景中才会使用,而且这种操作方式的硬件都是定制的。

4.异步访问文件的方式

当访问数据的线程发出请求后,线程会接着去处理其他事情,

而不是阻塞等待,当请求的数据返回后继续处理下面的操作。

这种访问文件的方式可以明显的提高应用程序的效率,但是不会改变访问文件的效率。

5.内存映射的方式

操作系统将内存中某一块区域与磁盘中的文件关联起来,当要访问内存中的一段数据时,

转换为访问文件的某一段数据。

这种方式的目的是减少数据从内核空间缓存到用户空间缓存的数据复制操作,

因为这两个空间的数据是共享的。

现在还有一个关键问题那就是数据写到何处,其中一个主要的方式就是将数据持久化到物理磁盘。

Java从磁盘读取文件的流程

序列化

简单地讲就是将一个对象转换成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化目的。需要

达到持久化,必须继承java.io.Serializable接口。

当然,序列化有很多不足的地方,下面是一些复杂情况下的问题: