Java IO 思想的简单笔记

Fifnmar edited 4 年,4 月前

[TOC]

写在前面

这篇文章只是大概讲解 Java IO 设计背后的逻辑和思想,结合了一点点历史,但并不直接讲 Java IO 的内容和使用方法。本文的目的在于帮助学习者理清学习脉络。

IO 的特点

IO, Input/Output,泛指程序的输入输出,即一个程序与外交流的功能。这样基础的功能,其重要性不言而喻。这里我总结 IO 功能的特点如下:

Java IO 的解决方案

Java IO 作为一个整体,联系紧密,不过还是可以大概拆成 IO Stream, Reader / Writer, NIO 三个部分。Java 还提供了转换三者的接口。

Java IO Stream
整体思路

前文说过,IO 功能多样,使用环境复杂。如果把这多样的功能都集成在单一的某个对象里,它的接口将异常膨胀。如此,不但非常难以使用,而且非常 Heavy-weight,效率也会被拖累(说的就是你 C++ <iostream>!你还好意思说 You don’t pay for what you don’t use 吗???)。所以,必须提供这样的能力:使用者可以自由决定使用哪些功能,不用哪些功能;使用者不需要的功能不应该被集成到 IO 对象中去。

解决方法是「装饰者模式」(Decorator Pattern)。简单讲就是首先提供一个 Core 核心组件,这个组件提供最最原始的功能。然后提供各种各样的小插件,使用者可以自由地把这些插件插在 Core 上,从而得到更多的功能。使用者没有插上的组件,也不会为 IO 对象增添任何负担。也就是说,装饰者模式使得使用者可以 DIY 自己的 IO 类。

Java IO Stream 是基于 Byte 的。基本的 IO Stream 以一个 Byte 为单位进行读写。

哪些是基本 Stream,哪些是装饰者

在上一节的整体思想下,Java 设计出了 IO Stream 系统。

(图被吃了.jpg)

InputStream 为例。一个基本的 InputStream 只提供原始的读入接口。但请注意那个 FilterInputStream——它就是各种插件的基类。其它 InputStream 负责从各种各样的源头读入字节,而 FilterInputStream 的子类为它们加上各种功能。例如,BufferedInputStream 接受一个 InputStream,并为它加上一个缓冲池,从而提高其效率(原始的 InputStream 没有缓冲)。

这样看来,或许应该把图换个方式画,基础的 InputStream 放在一边,FilterInputStream 放另一边。

下面特别地讲一下格式化 IO。

格式化输入输出

这里有点历史遗留问题,要结合历史才好理解。你或许会愿意先去了解一些「文本编码」(Character Encoding) 的背景知识;至少要知道世界上存在 ASCII, UTF-8, UTF-16, GB18030 等多种编码方式,而不同编码方式对同样的字符,比如 ñ,可能有不同的二进制表示,甚至没有对应表示。

格式化 IO 指的是把有格式的文本输入转成可用数据,或者把数据转成有格式的文本输出。

Java 的 IO Stream 是基于字节的。可以注意到 Java 提供了 DataInputStreamDataOutputStream,它们可以把几个字节转成某种基本类型。但不要以为它们能够格式化输入输出:它们只是方便了转换类型的过程。比如,手动地把读入的 8 个字节转成 double。但它不能正确处理 ASCII 文本 12.450(这是六个字符)。

输入输出往往是带格式的。为此,早期的 Java 为输入和输出分别提供了不同支持。请留意,下面两小节讲的是格式化设施,它们本身不负责具体的 IO!

格式化输入

Java 提供了一个简易解析器 Scanner,创建时需要提供一个输入源,本章头提到的三套设施均可。它使用正则表达式指定分隔符 (Delimiter)。分隔符默认是空白,可以用 useDelimiter() 方法重设其分隔符。

Scanner 根据分隔符把输入分成一个个小部分,用 next() 成员函数来获取下一个部分(是个 String),也可以使用 nextInt 等方法来顺便解析下一段 String

也可以手动调用 Integer 类提供的方法来把 String 翻译成 int

格式化输出

你可能注意到 Output Stream 里有个插件叫 PrintStream,没错!它提供了格式化功能,如 printLn, print, printf 等函数。你或许已经用过 System.out 了,这就是一个 PrintStream

我们知道 IO Stream 是基于 Byte 输出的。在早期,PrintStream 使用平台默认编码。这带来了巨大的乱码问题,比如,中国 Windows 上默认编码可能是 GBK,而其它某个系统的默认编码可能是 UTF-8。

Java 在 1.1 版本中打了个补丁,这就是下一节要讲的 Reader / Writer。

Java Reader / Writer

先讲一下字节和字符的简单区别。如上面「格式化输入输出」一节说到的,应该先了解一下什么是字符编码。

字节和字符

字节是一个固定大小的单位:8 个 bit(不要杠有的平台上 1 byte != 8 bit……);而一个字符是基于人类阅读的标准,比如「我」字是一个字符(不要杠不可见字符和 Unicode 组合字符……) 。在不同的编码方案中,一个字符的二进制表示可能是不同的。

整体思路

Java 最早的 IO 是基于 Byte 的二进制流。但它对于处理文本比较乏力。但处理文本的确是刚需,所以 Java 引入了一个「IO 体系的外来者」Scanner 和一个「不能处理编码问题」的 Print Stream,一直以来勉强能用。

Java 从 1.1 版本起打了补丁,就是 Reader / Writer。它们和 IO Stream 的不同在于,Reader / Writer 读写的基本单位是一个字符,而不是一个字节。当然,创建它们的时候要指定编码。

这个补丁直到 1.4 版本才打完整……

IO Stream 和 Reader / Writer 可以转化。举例:

// System.in 是一个 InputStream
var in = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));

下面是标准库预先定义的所有字符编码集:

Charset Description
US-ASCII Seven-bit ASCII, aka. ISO646-US, aka. the Basic Latin block of the Unicode character set
ISO-8859-1 ISO Latin Alphabet No. 1, aka. ISO-LATIN-1
UTF-8 Eight-bit UCS Transformation Format
UTF-16BE Sixteen-bit UCS Transformation Format, big-endian byte order
UTF-16LE Sixteen-bit UCS Transformation Format, little-endian byte order
UTF-16 Sixteen-bit UCS Transformation Format, byte order identified by an optional byte-order mark
我为什么不多讲

因为做 Java 的大佬们发现 IO Stream 用得太广泛了,PrintStream 虽然是个明显的失败设计,但也不能删。最后在 Java 10 中(出乎意料地晚)他们给 IO Stream 也加上了指定字符集的功能。

换句话说,现在 PrintStreamPrintWriter 几乎一毛一样。更深的是,由于 PrintStream 是基于字节的,所以 PrintStream 还能一个字节一个字节地输出,这是 PrintWriter 做不到的。

Emmm(地铁看手机老人脸)

鉴于现在 Java 14 都出了,LTS JDK 已经是 Java 11 而不是 Java 8 了,所以我想用 OutputStream 基本 OK。当然据说很多公司还在抱着古董级 Java 8,emmm,那我也没办法了 Orz。

NIO

Java 在 1.4 版本以后又引入了一套 IO:NIO。这套 IO 系统主要应对更复杂的 IO 场景,我今天懒了,先不讲了。它主要有四个部分:

当然 NIO 也可以和前面两者转换。

Bibliography

Comments