Java IO机制分析

Java 的 I/O 类库的基本架构

    Java 的 I/O 操做类大部分在包 java.io 下,这些类大概能够分红四组,分别是:java

  1. 基于字节操做的 I/O 接口:InputStream 和 OutputStream
  2. 基于字符操做的 I/O 接口:Writer 和 Reader
  3. 基于磁盘操做的 I/O 接口:File
  4. 基于网络操做的 I/O 接口:Socket

前两组主要是根据传输数据的数据格式,后两组主要是根据传输数据的方式,虽然 Socket 类并不在 java.io 包下,可是我仍然把它们划分在一块儿,由于我我的认为 I/O 的核心问题要么是数据格式影响 I/O 操做,要么是传输方式影响 I/O 操做,也就是将什么样的数据写到什么地方的问题,I/O 只是人与机器或者机器与机器交互的手段,除了在它们可以完成这个交互功能外,咱们关注的就是如何提升它的运行效率了,而数据格式和传输方式是影响效率最关键的因素了。咱们后面的分析也是基于这两个因素来展开的。缓存

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

基于字节的 I/O 操做接口输入和输出分别是:InputStream 和 OutputStream,InputStream 输入流的类继承层次以下图所示:网络

图 1. InputStream 相关类层次结构(查看大图数据结构

图 1. InputStream 相关类层次结构

输入流根据数据类型和操做方式又被划分红若干个子类,每一个子类分别处理不一样操做类型,OutputStream 输出流的类层次结构也是相似,以下图所示:架构

图 2. OutputStream 相关类层次结构(查看大图app

图 2. OutputStream 相关类层次结构

这里就不详细解释每一个子类如何使用了,若是不清楚的话能够参考一下 JDK 的 API 说明文档,这里只想说明两点,一个是操做数据的方式是能够组合使用的,如这样组合使用编码

OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream("fileName"))spa

还有一点是流最终写到什么地方必需要指定,要么是写到磁盘要么是写到网络中,其实从上面的类图中咱们发现,写网络实际上也是写文件,只不过写网络还有一步须要处理就是底层操做系统再将数据传送到其它地方而不是本地磁盘。关于网络 I/O 和磁盘 I/O 咱们将在后面详细介绍。操作系统

基于字符的 I/O 操做接口

不论是磁盘仍是网络传输,最小的存储单元都是字节,而不是字符,因此 I/O 操做的都是字节而不是字符,可是为啥有操做字符的 I/O 接口呢?这是由于咱们的程序中一般操做的数据都是以字符形式,为了操做方便固然要提供一个直接写字符的 I/O 接口,如此而已。咱们知道字符到字节必需要通过编码转换,而这个编码又很是耗时,并且还会常常出现乱码问题,因此 I/O 的编码问题常常是让人头疼的问题。关于 I/O 编码问题请参考另外一篇文章 《深刻分析Java中的中文编码问题》设计

下图是写字符的 I/O 操做接口涉及到的类,Writer 类提供了一个抽象方法 write(char cbuf[], int off, int len) 由子类去实现。

图 3. Writer 相关类层次结构(查看大图

图 3. Writer 相关类层次结构

读字符的操做接口也有相似的类结构,以下图所示:

图 4.Reader 类层次结构(查看大图

图 4.Reader 类层次结构

读字符的操做接口中也是 int read(char cbuf[], int off, int len),返回读到的 n 个字节数,不论是 Writer 仍是 Reader 类它们都只定义了读取或写入的数据字符的方式,也就是怎么写或读,可是并无规定数据要写到哪去,写到哪去就是咱们后面要讨论的基于磁盘和网络的工做机制。

字节与字符的转化接口

另外数据持久化或网络传输都是以字节进行的,因此必需要有字符到字节或字节到字符的转化。字符到字节须要转化,其中读的转化过程以下图所示:

图 5. 字符解码相关类结构

图 5. 字符解码相关类结构

InputStreamReader 类是字节到字符的转化桥梁,InputStream 到 Reader 的过程要指定编码字符集,不然将采用操做系统默认字符集,极可能会出现乱码问题。StreamDecoder 正是完成字节到字符的解码的实现类。也就是当你用以下方式读取一个文件时:

清单 1.读取文件

1

2

3

4

5

6

7

8

9

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,只不过这里的解码字符集是默认字符集。

写入也是相似的过程以下图所示:

图 6. 字符编码相关类结构

图 6. 字符编码相关类结构

经过 OutputStreamWriter 类完成,字符到字节的编码过程,由 StreamEncoder 完成编码过程。

磁盘 I/O 工做机制

前面介绍了基本的 Java I/O 的操做接口,这些接口主要定义了如何操做数据,以及介绍了操做两种数据结构:字节和字符的方式。还有一个关键问题就是数据写到何处,其中一个主要方式就是将数据持久化到物理磁盘,下面将介绍如何将数据持久化到物理磁盘的过程。

咱们知道数据在磁盘的惟一最小描述就是文件,也就是说上层应用程序只能经过文件来操做磁盘上的数据,文件也是操做系统和磁盘驱动器交互的一个最小单元。值得注意的是 Java 中一般的 File 并不表明一个真实存在的文件对象,当你经过指定一个路径描述符时,它就会返回一个表明这个路径相关联的一个虚拟对象,这个多是一个真实存在的文件或者是一个包含多个文件的目录。为什么要这样设计?由于大部分状况下,咱们并不关心这个文件是否真的存在,而是关心这个文件到底如何操做。例如咱们手机里一般存了几百个朋友的电话号码,可是咱们一般关心的是我有没有这个朋友的电话号码,或者这个电话号码是什么,可是这个电话号码到底能不能打通,咱们并非时时刻刻都去检查,而只有在真正要给他打电话时才会看这个电话能不能用。也就是使用这个电话记录要比打这个电话的次数多不少。

什么时候真正会要检查一个文件存不存?就是在真正要读取这个文件时,例如 FileInputStream 类都是操做一个文件的接口,注意到在建立一个 FileInputStream 对象时,会建立一个 FileDescriptor 对象,其实这个对象就是真正表明一个存在的文件对象的描述,当咱们在操做一个文件对象时能够经过 getFD() 方法获取真正操做的与底层操做系统关联的文件描述。例如能够调用 FileDescriptor.sync() 方法将操做系统缓存中的数据强制刷新到物理磁盘中。

下面以清单 1 的程序为例,介绍下如何从磁盘读取一段文本字符。以下图所示:

图 7. 从磁盘读取文件

图 7. 从磁盘读取文件

当传入一个文件路径,将会根据这个路径建立一个 File 对象来标识这个文件,而后将会根据这个 File 对象建立真正读取文件的操做对象,这时将会真正建立一个关联真实存在的磁盘文件的文件描述符 FileDescriptor,经过这个对象能够直接控制这个磁盘文件。因为咱们须要读取的是字符格式,因此须要 StreamDecoder 类将 byte 解码为 char 格式,至于如何从磁盘驱动器上读取一段数据,由操做系统帮咱们完成。至于操做系统是如何将数据持久化到磁盘以及如何创建数据结构须要根据当前操做系统使用何种文件系统来回答,至于文件系统的相关细节能够参考另外的文章。

参考文档:

https://www.ibm.com/developerworks/cn/java/j-lo-javaio/ 

相关文章
相关标签/搜索