0%

Java专题:IO流

本文主要介绍 Java IO 相关的基本概念、本地输入输出相关的接口。
并未深入讨论流的工作方式与模型,网络编程以及较高级的应用部分也未涉及。

概念

流:数据字节序列的抽象概念。

节点流类:用于直接操作目标设备所对应的流类。

流节点:节点流类所对应的IO源或目标。

输入流:程序可以从中读取数据的流。

输出流:程序能向其中写入数据的流。

字节流:以字节为单位传输数据的流。

字符流:以字符为单位传输数据的流。

节点流:用于直接操作目标设备的流,直接从一个源读写数据的流(没有经过包装和修饰,即底层流或原始流)。

包装流:Wrapper 流对象可能内含一个底层流对象的引用(reference),即所谓包装(wrap)了一个底层流对象。 用以提供过滤(filtered)、缓冲(buffered)等附加功能。

过滤流:过滤流包装另一个流,来提供附加的功能,或者以这种方式更改数据。

归纳

流的分类

按流向分:输入流、输出流

按数据传输单位分:字节流、字符流

按操作对象分:文件流、网络流、内存流、磁带流等

按是否包装分:原始流、包装流

JDK 中,流类被直观地从流向和传输单位等维度进行了划分。

类层次结构

说明

字符流

Java 中的字符流处理的最基本单元是 Unicode 码元(即 Unicode 代码单元,2 字节,0x0000~0xFFFF),通常用来处理文本数据。

Java 中的 String 类型默认就把字符以 Unicode 规则编码后存储在内存中。

与存储在内存中不同,存储在磁盘上的数据通常有着各种各样的编码方式。使用不同的编码方式,相同的字符会有不同的二进制表示。

字符流工作方式如下:

  • 输出字符流:把要写入文件的字符序列(Unicode 码元序列)转为指定编码方式下的字节序列,然后再写入到文件中;
  • 输入字符流:把要读取的字节序列按指定编码方式解码为相应字符序列(Unicode 码元序列)从而存入内存中。

由于字符流在输出前实际上要完成 Unicode 码元序列到相应编码方式的字节序列的转换,所以它会使用内存缓冲区来存放转换后得到的字节序列,等待都转换完毕再一同写入磁盘文件。

因此,字符流输出时,总需要指定目标编码方式,缺省情况下将使用系统默认编码。

对象流

将对象转换为一个字节序列,并写入到对象输出流中,称为“序列化”。序列化是 Java RMI(Remote Method Invocation)的技术,而 RMI 又是 Java EJB(Enterprise Java Bean)的基础。

包括:ObjectOutputStreamObjectInputStream

序列化

能被序列化的类需要实现标记接口 java.io.Serializable

如果被序列化的类中某个属性不需要序列化,则应使用 transient 修饰符修饰该属性。

序列化类的变更

如果类在序列化与反序列化的过程进行了修改,那么反序列化可能会出现问题。

Java 序列化使用 serialVersionUID 属性代表对象版本,不需要显式指定该属性,JVM 会基于一种算法来自动生成一个。

serialVersionUID 的生成算法会根据类名、实现的接口、成员变量、方法以及在庞大本地集群中的位置来计算。

serialVersionUID 是 64 位的长整型,称为流的唯一标识符(stream unique identifier)。

如果序列化与反序列化时,对应类的 serialVersionUID 属性不同,则反序列化时会出现 java.io.InvalidClassException

通常,建议自定义 serialVersionUID 属性,以便人为保证一些小更新版本间的兼容性。

但这样做的一个风险是,可能因为疏忽,在大的版本更新时忘记更新 serialVersionUID 属性,而出现错误。

手动维护 serialVersionUID 的一条经验规则是,无论任何时候添加或删除一个类特征(即属性或其他任何实例级状态变量),都需要重新维护。

比较

流 vs. 文件

文件是数据的静态存储形式,而流是指数据传输时的形态。文件只是流的操作对象之一。

字节流 vs. 字符流

InputStreamReaderOutputStreamWriter 是将字节流转换为字符流的桥梁(byte-to-character “bridge”)。

区别如下:

  • 字节流操作基本单元为字节,字符流操作的基本单元为 Unicode 码元。
  • 字节流默认不使用缓冲区,字符流使用缓冲区。
  • 字节流通常用于处理二进制数据,字符流通常处理文本数据。

附录

流的理解

之所以称为流,是因为这个数据序列在不同时刻所操作的是源的不同部分。

节点流:可理解为节点流量,节点流量表示一个节点上总体流量,节点流量跟方向无关,只要通过节点的单位量的集合就是节点流量。

高级的流

管道(piped)流,以成对的方式工作,被写入输出管道流的数据,会被与之相联的输入管道流读取。 包括:PipedInputStreamPipedOutputStreamPipedReaderPipedWriter

SequenceInputStream,使一组输入流的行为如同一个单独的输入流一样,依次逐个读取。

Pushback 流,主要用于词法分析程序,可将数据回放到流中,如同没有被读取一样。包括:PushbackInputStreamPushbackReader

StreamTokenizer,主要用于解析(parsing)的应用。它返回分词(token)的值和类型。分词类型可能是一个单词、数字、行结束标记或文件结束标记。

API 细节说明

字符流和字节流的 read() 方法都返回一个 int 值,但分别只用了低 16 位和低 8 位。

带缓冲的输入流仅在缓冲为空时调用本地输入 API;相对的,带缓冲的输出流仅在缓冲满时调用本地输出API。

所有的输出流都有 flush 方法,但除非该流是带缓冲的,否则不起作用。

PrintWriter.println() 方法输出的行终结符是平台相关的。行终结符可以是回车(carriage-return)/换行(line-feed)序列、单个回车、单个换行。

System.outPrintStream 对象,该类没有提供覆盖默认区域(Locale)的方法。

DataStream 依靠捕获 EOFException 来发现文件结束条件(end-of-file condition),而不是测试无效的返回值 -1。

Scanner 支持除 char 外所有 Java 原生类型、BigIntegerBigDecimal 作为 tokens。数字值可以用千分号,但这是区域(Locale)相关的。

默认 Scanner 使用空白符作为分隔标记。Character.isWhitespace() 给出了所有空白符。

虽然 Scanner 不是流,但仍应该关闭它,以明确底层的流操作已结束。