如果有人再问你 Java IO,把这篇文章砸他头上(一)

一、简介

说到 I/O,想必大家都不会陌生, I/O 英语全称:Input/Output,即输入/输出,通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出

比如我们常用的 SD 卡U 盘移动硬盘等等存储文件的硬件设备,当我们将其插入电脑的 usb 硬件接口时,我们就可以从电脑中读取设备中的信息或者写入信息,这个过程就涉及到 I/O 的操作。

如果有人再问你 Java IO,把这篇文章砸他头上(一)

当然,涉及 I/O 的操作,不仅仅局限于硬件设备的读写,还要网络数据的传输,比如,我们在电脑上用浏览器搜索互联网上的信息,这个过程也涉及到 I/O 的操作。

如果有人再问你 Java IO,把这篇文章砸他头上(一)

无论是从磁盘中读写文件,还是在网络中传输数据,可以说 I/O 主要为处理人机交互机与机交互中获取和交换信息提供的一套解决方案。

在 Java 的 IO 体系中,类将近有 80 个,位于java.io包下,感觉很复杂,但是这些类大致可以分成四组:

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

前两组主要从传输数据的数据格式不同,进行分组;后两组主要从传输数据的方式不同,进行分组。

虽然 Socket 类并不在java.io包下,但是我们仍然把它们划分在一起,因为 I/O 的核心问题,要么是数据格式影响 I/O 操作,要么是传输方式影响 I/O 操作,也就是将什么样的数据写到什么地方的问题,I/O 只是人与机器或者机器与机器交互的手段,除了在它们能够完成这个交互功能外,我们关注的就是如何提高它的运行效率了,而数据格式传输方式是影响效率最关键的因素。

本文后面,也是基于这两个点进行深入展开分析。

二、基于字节操作的接口

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

2.1、字节输入流

InputStream 输入流的类继承层次如下图所示:如果有人再问你 Java IO,把这篇文章砸他头上(一)

输入流根据数据节点类型和处理方式,分别可以划分出了若干个子类,如下图:

如果有人再问你 Java IO,把这篇文章砸他头上(一)

OutputStream 输出流的类层次结构也是类似。

2.2、字节输出流

OutputStream 输出流的类继承层次如下图所示:如果有人再问你 Java IO,把这篇文章砸他头上(一)

输出流根据数据节点类型和处理方式,也分别可以划分出了若干个子类,如下图:

如果有人再问你 Java IO,把这篇文章砸他头上(一)

在这里就不详细的介绍各个子类的使用方法,有兴趣的朋友可以查看 JDK 的 API 说明文档,笔者也会在后期的文章会进行详细的介绍,这里只是重点想说一下,无论是输入还是输出,操作数据的方式可以组合使用,各个处理流的类并不是只操作固定的节点流,比如如下输出方式:

//将文件输出流包装到序列化输出流中,再将序列化输出流包装到缓冲中
OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream(new File("fileName")));

另外,输出流最终写到什么地方必须要指定,要么是写到硬盘中,要么是写到网络中,从图中可以发现,写网络实际上也是写文件,只不过写到网络中,需要经过底层操作系统将数据发送到其他的计算机中,而不是写入到本地硬盘中。

三、基于字符操作的接口

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符,但是为什么要有操作字符的 I/O 接口呢?

这是因为我们的程序中通常操作的数据都是以字符形式,为了程序操作更方便而提供一个直接写字符的 I/O 接口,仅此而已。

基于字符的输入和输出操作接口分别是:Reader 和 Writer ,下图是字符的 I/O 操作接口涉及到的类结构图。

3.1、字符输入流

Reader 输入流的类继承层次如下图所示:

如果有人再问你 Java IO,把这篇文章砸他头上(一)

同样的,输入流根据数据节点类型和处理方式,分别可以划分出了若干个子类,如下图:如果有人再问你 Java IO,把这篇文章砸他头上(一)

3.2、字符输出流

Writer 输出流的类继承层次如下图所示:如果有人再问你 Java IO,把这篇文章砸他头上(一)

同样的,输出流根据数据节点类型和处理方式分类,分别可以划分出了若干个子类,如下图:

如果有人再问你 Java IO,把这篇文章砸他头上(一)

不管是 Reader 还是 Writer 类,它们都只定义了读取或写入数据字符的方式,也就是说要么是读要么是写,但是并没有规定数据要写到哪去,写到哪去就是我们后面要讨论的基于磁盘或网络的工作机制。

四、字节与字符的转化

刚刚我们说到,不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,设计字符的原因是为了程序操作更方便,那么怎么将字符转化成字节或者将字节转化成字符呢?

InputStreamReader 和 OutputStreamWriter 就是转化桥梁。

4.1、输入流转化过程

输入流字符解码相关类结构的转化过程如下图所示:如果有人再问你 Java IO,把这篇文章砸他头上(一)

从图上可以看到,InputStreamReader 类是字节到字符的转化桥梁, 其中StreamDecoder指的是一个解码操作类,Charset指的是字符集。

InputStream 到 Reader 的过程需要指定编码字符集,否则将采用操作系统默认字符集,很可能会出现乱码问题,StreamDecoder 则是完成字节到字符的解码的实现类。

打开源码部分,InputStream 到 Reader 转化过程,如下图:如果有人再问你 Java IO,把这篇文章砸他头上(一)


4.1、输出流转化过程

输出流转化过程也是类似,如下图所示:

如果有人再问你 Java IO,把这篇文章砸他头上(一)

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

源码部分,Writer 到 OutputStream 转化过程,如下图:如果有人再问你 Java IO,把这篇文章砸他头上(一)

五、基于磁盘操作的接口

前面介绍了 Java I/O 的操作接口,这些接口主要定义了如何操作数据,以及介绍了操作数据格式的方式:字节流和字符流。

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

我们知道数据在磁盘的唯一最小描述就是文件,也就是说上层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器交互的一个最小单元。

如果有人再问你 Java IO,把这篇文章砸他头上(一)

在 Java I/O 体系中,File 类是唯一代表磁盘文件本身的对象

File 类定义了一些与平台无关的方法来操作文件,包括检查一个文件是否存在、创建、删除文件、重命名文件、判断文件的读写权限是否存在、设置和查询文件的最近修改时间等等操作。

值得注意的是 Java 中通常的 File 并不代表一个真实存在的文件对象,当你通过指定一个路径描述符时,它就会返回一个代表这个路径相关联的一个虚拟对象,这个可能是一个真实存在的文件或者是一个包含多个文件的目录。

例如,读取一个文件内容,程序如下:如果有人再问你 Java IO,把这篇文章砸他头上(一)

以上面的程序为例,从硬盘中读取一段文本字符,操作流程如下图:

如果有人再问你 Java IO,把这篇文章砸他头上(一)

我们再来看看源码执行流程。

当我们传入一个指定的文件名来创建 File 对象,通过 FileReader 来读取文件内容时,会自动创建一个FileInputStream对象来读取文件内容,也就是我们上文中所说的字节流来读取文件。

如果有人再问你 Java IO,把这篇文章砸他头上(一)

紧接着,会创建一个FileDescriptor的对象,其实这个对象就是真正代表一个存在的文件对象的描述。可以通过FileInputStream对象调用getFD()方法获取真正与底层操作系统关联的文件描述。如果有人再问你 Java IO,把这篇文章砸他头上(一)

由于我们需要读取的是字符格式,所以需要 StreamDecoder 类将byte解码为char格式,至于如何从磁盘驱动器上读取一段数据,由操作系统帮我们完成。

上一篇:jQuery之表格变色


下一篇:记录一次在Windows中安装Anaconda3的详细过程