IO即数据读写。数据是应用的中心要素,而数据读写的能力和可扩展性是编程平台的基础支撑。
概念框架
方式: 字节流 Byte 和 字符流 Char
方向: 输入 Input 和 输出 Output ; 读 Reader 和 写 Writer
源: 字符串 String, 数组 Array, 对象 Object, 文件 File, 通道 Channel, 管道 Pipe, 过滤器 Filter,控制台 Console, 网络 Network Link ; 一切可产生/接收数据的 Data
性能: 带缓冲和不带缓冲的
行为: Readable, Appendable, Closable, Flushable, Serializable/Externalizable
概念组合
方式、方向、源、行为四个维度可以组合出各种特性的IO流。JavaIO框架采用装饰器模式,可以在现有IO流的基础上添加新的IO特性,生成更强功能的IO流,体现了读写能力的可扩展性。
比如要构造一个带缓冲的文本文件读取流: File -> FileInputStream -> InputStreamReader -> FileReader -> BufferedFileReader . 【文本:字符流, 文件:源, 读:输入方向, 带缓冲的:性能】
读写耗时的字符输入输出流可以使用缓冲来提升性能,比如文件读写、网络流读写等。这是由于每个待读写的字符都要经过【读写字节、字节/字符转换】两个操作。缓冲实际上是将大量单个的读写/转换或多个小粒度的读写/转换操作,转换为一个大的单个的批量读写/转换操作。
字节流
字节输入流:InputStream, BufferedInputStream, FileInputStream, ByteArrayInputStream, PipedInputStream, FilterInputStream, DataInputStream, ObjectInputStream;
字节输出流: OuputStream, BufferedOuputStream, FileOuputStream, ByteArrayOuputStream, PipedOuputStream, FilterOuputStream, DataOuputStream, ObjectOuputStream, PrintStream;
字符流
字符输入流: Reader, BufferedReader, FileReader, CharArrayReader, PipedReader, FilterReader, StringReader, LineNumberReader;
字符输出流: Writer, BufferedWriter, FileWriter, CharArrayWriter, PipedWriter, FilterWriter, StringWriter, PrintWriter;
字节流与字符流之间的转化
InputStreamReader:输入字节流转化为输入字符流
OutStreamWriter: 输出字符流转化为输出字节流
其它
Bits: 原子类型 boolean, short, int, long, float, double 到其字节数组表示之间的转化(大端字节序);将原子类型转化为字节数组使用 >>>8 运算符,高位字节在数组低索引; 将字节数组转化为原子类型则采用 [&0xFF, <<8, +] 运算符 。
装饰器模式:多个类实现同一个接口,并且这些实现类均持有一个该接口的引用,通过构造器传入,从而可以在这些实现类的基础上任意动态地组合叠加,构造出所需特性的实现来。装饰器模式可以实现数学公式/逻辑公式运算函数。
实现
- 在IO流的实现类中,通常有一个 【Char[],Byte[], CharBuffer, StringBuffer, ByteBuffer】的成员数组或成员对象。将成员数组/对象里的数据写到方法里给定的参数数组或返回值,即为读实现;将方法里给定的参数数组写到成员数组或成员对象里,即为写实现。读写数据本质上就是数据在不同源之间的拷贝实现。
- IO流通常是会实现读写单个字节/字符、读写字节数组/字符数组、跳过若干字节/字符的功能。成员 【next,pos,mark,count】 用于标识数据读写位置;mark 与 reset 方法组合可实现重复读写。
- 要实现一个自定义的 Reader, 可参考 StringReader 实现;StringReader 可以将字符串作为字符流来读取,内部成员为String;而 StringWriter 内部成员为线程安全的 StringBuffer。两者均要考虑并发读写的问题,使用一个 Object 锁进行互斥访问。
- FileReader 是文本文件读取流,通过继承 InputStreamReader, 转化 FileInputStream 而得。因文件是对磁盘字节数据的抽象,因此要获得人类可读的文本,必然存在从字节流到字符流的转换。
- InputStreamReader 使用 StreamDecoder 将字节流转换为指定字符编码的字符流,后续读操作直接委托 StreamDecoder 读取;StreamDecoder 是一个字节解码器, 是 Reader 实现类。OutputStreamWriter 使用 StreamEncoder 将字符流转换为指定字符编码的字节流,后续写操作直接委托 StreamEncoder 写入底层 OutputStream;StreamEncoder 是一个 Writer 实现类。
- FilterReader, FilterWriter, FilterInputStream, FilterOutputStream: IO流的抽象类,分别持有一个【Reader,Writer,InputStream, OutputStream】, 自定义 IO 流可以通过继承 FilterXXX 来实现。CommonIO库里 ProxyReader, ProxyWriter 分别提供了 Reader, Writer 的一个示例实现, java.io.DataInputStream 提供了 FilterInputStream 的一个示例实现,java.io.PrintStream 提供了 FilterOutputStream 的一个示例实现。
- BufferedReader: 提供了缓冲读及读行的功能。BufferedReader 主要成员为 【Reader in, char[defaultsize=8192] cb, int nextChar, int nChars, int markedChar】; nextChar 指示将要读取的字符位置, nChars 指示可以读取字符的终止位置, markedChar 表示被标记的将要读取字符的位置; BufferedReader 从字符流 in 中读取字符预存入缓冲字符数组 cb 中,然后在需要的时候从 cb 中读取字符。BufferedReader 的实现由 fill 和 read, readLine 共同完成。当缓冲字符数组的字符数不足于填充要读取的参数数组时,就会使用 fill 方法从输入流 in 中读取数据再进行填充;使用 System.arraycopy 方法
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); 拷贝字符。
- PipedReader: 管道输入流,必须从 PipedWriter 中读取。底层实现有两个线程、两个位置索引以及一个字符数组。两个线程分别是读线程 readSide 和 写线程 writeSide, 两个位置索引分别用于指示字符数组中将要写的位置和将要读的位置。字符数组是一个循环队列,默认大小为 1024 chars 。初始化时必须将 PipedReader 的输入连接至 PipedWriter 。读实现方法中,即是将底层字符数组拷贝到方法中的参数数组。PipedWriter 的实现相对简单,其输出连接至 PipedReader; PipedWriter 的写就是 PipedReader 的读,因此 PipedWriter 的方法实现委托给 PipedReader 引用的方法。
文件
文件相关的类: File, RandomAccessFile, FileDescriptor, FileChannel, FilePermission, FilePermissionCollection, FileFilter, FileSystem, UnixFileSystem ;
文件 File: 唯一性路径标识【目录路径/文件名[.文件扩展名]】、文件元信息【描述符、类型、大小、创建时间、最近访问时间、节点信息】、文件访问权限【读/写/执行组合】等; File 的操作会先使用 SecurityManager 检查访问权限,然后通过 FileSystem 判断和操作。
构造可以实际读写的文件输入输出流需要使用 FileInputStream, FileOutputStream, FileReader, FileWriter, BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream 来包装 File ,比如 new BufferedReader(new FileReader(new File(filename))) 。
FileChannel: 操作文件内容的通道;FileChannel 是线程安全的,使用 java.nio 引入的高性能机制来实现。
文件描述符 FileDescriptor : 用于操作打开的文件、网络socket、字节流的唯一标识句柄;其成员是一个整型数 fd 和一个原子计数器 AtomicInteger useCount ; 标准输入流 System.in.fd = 0 , 标准输出流 System.out.fd = 1 , 标准错误流 System.err.fd = 2
文件访问权限 FilePermission: 提供了【文件可进行的操作 actions 与位掩码表示 mask 】之间的转化方法。文件可进行的操作 actions 在类 SecurityConstants 的常量中定义; mask 是一个整型数,表示文件的执行(0x01)/写(0x02)/读(0x04)/删(0x08)/读链接(0x10)/无(0x00) 等权限组合。
FileSystem:所使用的操作系统平台的文件系统。指明该文件系统所使用的【文件分隔符、路径分隔符、路径标准化、路径解析、文件访问安全控制、文件元信息访问、文件操作等】。文件分割符是指路径名中分割目录的符号,linux = '/', windows = '\' ; 路径分割符是环境变量中分割多个路径的符号,linux = ':' , windows = ';' ; UnixFileSystem 提供了 FileSystem 的一个实现示例。
枚举: 存在 BA_EXISTS = 0x01; 普通文件 BA_REGULAR = 0x02; 目录 BA_DIRECTORY = 0x04; 隐藏 BA_HIDDEN = 0x08; 可读 ACCESS_READ = 0x04; 可写 ACCESS_WRITE = 0x02; 可执行 ACCESS_EXECUTE = 0x01; 判断文件属性及访问权限采用与或非位运算实现, 优点是可以灵活组合。
序列化
序列化是实现内存数据持久化的机制。可以将 Java 对象数据存储到文件中,或者将文件中的数据恢复成Java对象。Java RPC, Dubbo,ORM 等都依赖于序列化机制。
序列化相关的类及方法: Serializable/Externalizable, ObjectInputStream, ObjectOutputStream, readObject, writeObject 。
- 使用 ObjectInputStream.readObject, ObjectOutputStream.writeObject 方法进行对象序列化: JDK提供的标准对象序列化方式;对象必须实现 Serializable 接口;适用于任何 Java 对象图的序列化,通用的序列化方案;输出为可读性差的二进制文件格式,很难为人所读懂,无法进行编辑;对某些依赖于底层实现的组件,存在版本兼容和可移植性问题;只有基于 Java 的应用程序可以访问序列化数据;
- 使用 XMLDecoder.readObject, XMLEncoder.writeObject 方法进行对象序列化: 对象是普通的 javaBean,无需实现 Serializable 接口;输出为可读性良好的XML文件,容易编辑和二次处理;其它平台亦可访问序列化数据;可以修改数据成员的内部结构的实现,只要保留原有bean属性及setter/getter签名,就不影响序列化和反序列化;可以解决版本兼容的问题,提供长期持久化的能力;每个需要持久化的数据成员都必须有一个 Java bean 属性。
- 实现 Externalizable 接口来进行对象序列化。需在对象中实现 readObject, writeObject 方法,提供更高的序列化灵活性,由开发者自行决定实例化哪些字段、何种顺序、输出格式等细节。
NewIO
java.nio 为 javaIO 体系引入了更好的性能改善,主要是 Buffer, Channel 和 file 相关的支撑类。由于 java.io 类已经引用了 nio 里的类,因此直接使用 java.io 里的类就可以获得 nio 带来的好处了。
nio后续再完善。
代码阅读小记
理解框架的关键在于理清其设计思路及概念结构(概念及其关联)。
阅读 JDK 代码可以获得很多面向对象的软件框架设计的有益启示。