本文主要介绍 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)的基础。
包括:ObjectOutputStream
、ObjectInputStream
。
序列化
能被序列化的类需要实现标记接口 java.io.Serializable
。
如果被序列化的类中某个属性不需要序列化,则应使用 transient
修饰符修饰该属性。
序列化类的变更
如果类在序列化与反序列化的过程进行了修改,那么反序列化可能会出现问题。
Java 序列化使用 serialVersionUID
属性代表对象版本,不需要显式指定该属性,JVM 会基于一种算法来自动生成一个。
serialVersionUID
的生成算法会根据类名、实现的接口、成员变量、方法以及在庞大本地集群中的位置来计算。
serialVersionUID
是 64 位的长整型,称为流的唯一标识符(stream unique identifier)。
如果序列化与反序列化时,对应类的 serialVersionUID
属性不同,则反序列化时会出现 java.io.InvalidClassException
。
通常,建议自定义
serialVersionUID
属性,以便人为保证一些小更新版本间的兼容性。但这样做的一个风险是,可能因为疏忽,在大的版本更新时忘记更新
serialVersionUID
属性,而出现错误。手动维护
serialVersionUID
的一条经验规则是,无论任何时候添加或删除一个类特征(即属性或其他任何实例级状态变量),都需要重新维护。
比较
流 vs. 文件
文件是数据的静态存储形式,而流是指数据传输时的形态。文件只是流的操作对象之一。
字节流 vs. 字符流
InputStreamReader
、OutputStreamWriter
是将字节流转换为字符流的桥梁(byte-to-character “bridge”)。
区别如下:
- 字节流操作基本单元为字节,字符流操作的基本单元为 Unicode 码元。
- 字节流默认不使用缓冲区,字符流使用缓冲区。
- 字节流通常用于处理二进制数据,字符流通常处理文本数据。
附录
流的理解
之所以称为流,是因为这个数据序列在不同时刻所操作的是源的不同部分。
节点流:可理解为节点流量,节点流量表示一个节点上总体流量,节点流量跟方向无关,只要通过节点的单位量的集合就是节点流量。
高级的流
管道(piped)流,以成对的方式工作,被写入输出管道流的数据,会被与之相联的输入管道流读取。 包括:PipedInputStream
、PipedOutputStream
、PipedReader
、PipedWriter
。
SequenceInputStream
,使一组输入流的行为如同一个单独的输入流一样,依次逐个读取。
Pushback 流,主要用于词法分析程序,可将数据回放到流中,如同没有被读取一样。包括:PushbackInputStream
、PushbackReader
。
StreamTokenizer
,主要用于解析(parsing)的应用。它返回分词(token)的值和类型。分词类型可能是一个单词、数字、行结束标记或文件结束标记。
API 细节说明
字符流和字节流的 read() 方法都返回一个 int 值,但分别只用了低 16 位和低 8 位。
带缓冲的输入流仅在缓冲为空时调用本地输入 API;相对的,带缓冲的输出流仅在缓冲满时调用本地输出API。
所有的输出流都有 flush
方法,但除非该流是带缓冲的,否则不起作用。
PrintWriter.println()
方法输出的行终结符是平台相关的。行终结符可以是回车(carriage-return)/换行(line-feed)序列、单个回车、单个换行。
System.out
是 PrintStream
对象,该类没有提供覆盖默认区域(Locale)的方法。
DataStream
依靠捕获 EOFException
来发现文件结束条件(end-of-file condition),而不是测试无效的返回值 -1。
Scanner
支持除 char
外所有 Java 原生类型、BigInteger
和 BigDecimal
作为 tokens。数字值可以用千分号,但这是区域(Locale)相关的。
默认 Scanner
使用空白符作为分隔标记。Character.isWhitespace()
给出了所有空白符。
虽然 Scanner
不是流,但仍应该关闭它,以明确底层的流操作已结束。