[三]JavaIO之IO体系类整体设计思路 流的概念以及四大基础分类
从本文开始,将正式进入JavaIO的简介
在继续javaIO系列的文章之前
可以过去看一下 本人博客上的设计模式中的 适配器模式和装饰器模式
这会对接下来的阅读大有帮助
本文是从逻辑上介绍整个的Java IO类家谱画像.
1.流
计算机以及互联网的世界发展不过短短几十年,但是这几十年却是日新月异
系统的复杂度也越来越高,程序设计语言的抽象程度也越来越高
但是无论如何都绕不开一个话题,那就是IO
之前已经介绍过,IO 就是输入 和 输出, 入和出是相对于应用程序来说的
而且,经常一个程序的输出可能是另一个程序的输入
这本身就是一个抽象的概念
并没有"必须怎么样,那才叫IO"的说法
从数据库,从文件,从内存,从网络拿数据,你都可以叫做输入,数据写出,都可以叫做输出,这并没有什么好纠结的
在java中使用流这一概念来描述输入和输出
流的字面含义
百度百科中是这样描述流的, 可以看得出来 , 流本身就包含了 这样一层含义
物质 一个地方 流向了 另一个地方 |
在继续之前,我们先回想下放暑假或者开学时候的场景
假定你需要做汽车和火车,如下图所示
上图中有几个关键概念
主体 | 人 人从一个地方到了另一个地方 |
源/目的 | 学校/家 |
方向 | 回家或者返校 图中的两个箭头 |
中间形式 | 火车和汽车 |
1.1流到底是什么
我们再举一个比较简单的例子, 使用 水管 往桶里面加水或者抽水
1.2程序语言中的流的主要概念
含义/源/方向/数据形式/中间形式 |
流的含义:
在程序设计中,流是对于数据流动传输的一种抽象描述
任何有能力产出数据的数据源,或者有能力接受数据的接收端对象都是一个流 (也就是上面例子中的一个容器接上水管)
|
流的源和目的:
数据可能从本地文件读取,或者写入, 也可能发送到网络上,这就是源和目的
|
流的方向:
同水管里面的流水一样,也是只有两个方向,流进或者流出,也就是我们常说的输入 和 输出
|
流的数据形式: 数据的具体形式就是流传送的内容,可能是字节,也能是字符,这就是数据的形式 |
流的中间形式: 对于任何一个流对它的功能进行一些必要的扩充,就好像接上了转接头的流可以接到其他规格的水管一样 在一个流的基础上 包装,装饰上其他的一些功能,流就会变得更加强大 |
1.3 流相关概念详细解读
1.3.1 流的源和目的
1.文件
最基本的一个数据源就是我们前文提到过的文件,文件不仅java中有,其他语言中也拥有文件的概念
|
2.字节数组
数据最基本的单位是字节
数组是在程序设计中,为了处理方便, 把具有相同类型的若干变量按有序的形式组织起来的一种形式
这些按序排列的同类数据元素的集合称为数组
所以字节数组,自然是为了更方便操作字节的一种数据组织形式
|
3. 字符数组/String对象
既然数组可以简化更方便的进行操作,而且也有字节数组
是不是还应该有字符数组呢?
而且,java中的String对象 ,它的内部实现也是char数组,java中使用char表示字符,这不就是字符数组么
|
4. 管道
"管道"的概念也是类似字面含义,一端输入,就可以从一端流出,就好像一个水管一样,
主要用来多线程之间直接进行数据交互,所以说数据来源也可能是一个管道
|
5.网络等
其他数据源比如网络等,java的强项就是WEB,从网络接收数据是再自然不过的事情
|
6.流 另外流本身也可以作为一种源,所以一个流的源可以来自另外的一个流 |
1.3.2 流的方向
流的方向很简单,只有两个方向,输入 或者 输出
|
1.3.3 流的数据形式
计算机存储数据是二进制的 0 1 序列
计算机中存储容量的最小的单位是位(bit)最基本的单位是字节(byte)
字节是通过网络传输信息(或在硬盘或内存中存储信息)的单位
也就是说任何其他形式的数据,都可以而且,最终也都是用字节来表示
所以数据最基本的形式就是字节
1 byte = 8 bit
|
我们的世界充满了各种符号
字符是表示数据和信息的字母、数字或其他符号
在电子计算机中,每一个字符与一个二进制编码相对应,这是一个编码的过程
|
所以说,数据的基本形式有 字节 和 字符两种形式 |
1.3.4流的中间形式
放学回家的例子,我们很清楚的知道,火车和汽车是我们 人的中间形式过程,经过转换(买票上车), 地上的人看不到我们了,看到的只是火车 对于流来说,中间形式是什么样子的呢? 比如我们想要把一个Int类型直接写入到文件中,怎么办呢? 我们是不是需要把这个类型的数据处理下 转换下呢 或者说包装下 就如同你坐上了车(车把你装了进去,形式就是车),总之就是要处理下 比如想要缓冲,按照行,按照字等等 这就是一种中间形式,后面我们会详细介绍涉及到的中间形式 |
不过很显然,中间形式并没有向从某种数据源读取数据那么刚需 但是他会给你提供更多的功能,让你的流功能更加多变,扩展 如果有了中间形式,你可能就能够直接把一个int写入到文件上,这不是很方便么 |
1.3.5流的种类-基本功能 扩展功能
想要完成一个IO类库的基本功能,只需要把握住三点 |
1. 流的源和目的
2. 流的数据形式
3. 流的方向
|
想要做得更好就需要把握好流的中间形式,提供更强大的功能 |
流的源和目的 | 文件 / 字节数组 /管道 /字符数组/String对象 / 网络 / 流 |
流的数据形式 | 字符 / 字节 |
流的方向 | 输入 / 输出 |
现在我们掌握了流的基本属性,上表中的三种,也掌握了他们可能的变量值
很简单,只需要使用简单的组合进行计算,我们就可以列举出来所有可能的组合
下面我们试着列一些(并不会列出来全部内容)
文件(源) | 输入 | 字节 |
文件(源) | 输入 | 字符 |
文件(目的地) | 输出 | 字节 |
文件(目的地) | 输出 | 字符 |
字节数组(源) | 输入 | 字节 |
字节数组(源) | 输入 | 字符 |
字节数组(目的地) | 输出 | 字节 |
字节数组(目的地) | 输出 | 字符 |
管道(源) | 输入 | 字节 |
管道(源) | 输入 | 字符 |
管道(目的地) | 输出 | 字节 |
管道(目的地) | 输出 | 字符 |
等等................等等................
还有很多种组合, 我相信你肯定可以排列的出来
不过很显然,我们此处只是简单的罗列,穷举出所有组合的可能
对于类库的设计自然不能这么简单暴力,或许有些组合没有必要,或许有些组合不符合逻辑
去掉那些无用的,不合逻辑的,无意义的,那么剩下来的组合形式,其实就是IO类库要解决的问题
也就是就剩下了我们现在看得到的JavaIO类库了 接下来从整体上对IO类库进行介绍
2. JAVA IO类库体系结构
java.io
java.io包中(JDK8),有87个类,其中有一些辅助类 还有一些异常类
去掉这些之后,剩下的绝大多数都是IO类体系的直接相关类,看起来很杂乱繁多
我们接下来讲从整体上对涉及到的IO类进行介绍,等看完本篇文章,相信你应该能有一个整体的把控
只有从整体把控才有可能掌握整个完整的类家族
2.1 流的四大家族
如果先不考虑数据的来源,根据流的方向(输入 和 输出)以及流的数据形式(字符 和 字节) 我们有四种形式
输入 | 字节 |
输出 | 字节 |
输入 | 字符 |
输出 | 字符 |
四种形式 | 输入字节 | 输出字节 | 输入字符 | 输出字符 |
Java中名称 | InputStream | OutPutStream | Reader | Writer |
可以看得出来在命名上,类库设计者的一些想法
把字节使用Stream作为后缀,或许因为字节是最基本的单位,所以他才是流Stream
我们平时阅读 read和书写write的都是字符,所以使用Reader 和 Writer表示字符的输入和输出也很自然
|
节点流与过滤流
我们上面讲述流的含义概念时,反复提到了流的基本功能以及中间形式
基本功能就是针对于不同数据源的操作,属于刚需范围
而中间形式则是刚需的强有力的增强
流的数据源/目的 流的方向 流的数据类型的组合,构成了基本功能的完整集合
而对于增强型的流的形式,则是Java IO出彩的地方,属于增强型的功能
java中针对于基本数据源进行操作的流叫做 节点流
而对于那些起到增强装饰功能的流,叫做过滤流
按照我们上面的思维逻辑
只需要把相关的数据源与我们上面的这四种基本形式进行组合
就可以得到流的基本功能家族,也就是节点流
根据节点流需要的拓展功能,我们就可以推演出来过滤流
2.2 流体系类层次结构详解
2.2.1 InputStream
2.2.1.1 InputStream节点流
数据源与InputStream的结合
字节数组 | ByteArrayInputStream (java.io) |
文件 |
FileInputStream (java.io)
|
管道 |
PipedInputStream (java.io)
|
String |
|
对象 | ObjectInputStream (java.io) |
上面就是IO类库提供给我们的基础功能
也就是可用的有效的合理的数据源与InputStream的组合(InputStream 流的方向与流的数据形式的组合)
类名 | 功能 | 构造方法 |
ByteArrayInputStream | 从字节数组中读取数据,也就是从内存中读取数据 包含一个内部缓冲区,指向该字节数组
内部计数器跟踪 read 方法要提供的下一个字节
关闭 ByteArrayInputStream 无效
此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException
|
ByteArrayInputStream(byte buf[]) ByteArrayInputStream(byte buf[], int offset, int length) 不是复制而来,直接指向地址 多参数的带偏移量 |
FileInputStream | 用于从文件中读取信息 | FileInputStream(String name) FileInputStream(File file) FileInputStream(FileDescriptor fdObj) 使用文件路径名 抽象路径名File 或者文件描述符 |
PipedInputStream | 产生用于写入相关Pipe的OutputStream的数据 实现管道化的概念 管道输入流应该连接到管道输出流; 管道输入流提供要写入管道输出流的所有数据字节 通常,数据由某个线程从 PipedInputStream 对象读取 并由其他线程将其写入到相应的 PipedOutputStream 不建议对这两个对象尝试使用单个线程,因为这样可能死锁线程 |
PipedInputStream(PipedOutputStream src) PipedInputStream(PipedOutputStream src, int pipeSize) PipedInputStream() PipedInputStream(int pipeSize) |
弃用,如果条件允许可以考虑使用StringReader | ||
ObjectInputStream |
对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化
ObjectOutputStream 和 ObjectInputStream 分别与 FileOutputStream 和 FileInputStream 一起使用时
可以为应用程序提供对对象图形的持久存储
ObjectInputStream 用于恢复那些以前序列化的对象
其他用途包括使用套接字流在主机之间传递对象,或者用于编组和解组远程通信系统中的实参和形参。
|
ObjectInputStream(InputStream in) ObjectInputStream() |
SequenceInputStream可以说既不是节点流也不是过滤流,硬要算的话,可以说是节点流 算是一个工具类一样的存在
SequenceInputStream (java.io) | 两个或者多个InputStream对象转换为单一的InputStream | SequenceInputStream(InputStream s1, InputStream s2) SequenceInputStream(Enumeration<? extends InputStream> e) |
2.2.1.2 InputStream过滤流
介绍过了InputStream的节点流,我们看下,我们还希望InputStream能够哪些扩展的功能,也就是上面提到过的 流的中间形式
我们之前就提到过,希望能够有直接操作数据类型的流,通过这个流可以直接操作基本数据类型的读写,而不需要自己去处理字节或者字节数组等
也就是说我们希望能够对基本数据类型进行支持
|
IO是操作系统的瓶颈,如果过于频繁的直接对磁盘IO进行读写,势必会增加CPU的空闲,性能降低,我们希望能够有缓冲的功能
|
IDE开发工具的编辑器都有行号的标志,行号可以给我们提供很多的便捷性,所以希望能够跟踪展示行号 |
比如当我们用程序读取一行代码,识别其中的关键字 比如 int i = 0; 读取到int时,我们不知道他是不是关键字,可能是一个int0的变量名 读取到下一个的时候,发现是空格,我们才能确定,他就是一个关键字 但是下面的空格已经被读取了,我们可能希望接下来的扫描能够读取到空格,可是流是顺序的,被消费了就不存在了 所以希望能够把读取到的字节回退到原来的流中 |
于是就有了
支持基本数据类型/缓存/行号/回退 这几种扩展功能的想法
功能点和InputStream组合下可以得到如下四种扩展功能
Data表示基本数据类型 Buffer 表示缓冲 LineNumber表示行号 PushBack表示回退
DataInputStream (java.io)
BufferedInputStream (java.io)
LineNumberInputStream (java.io)
PushbackInputStream (java.io)
|
到底怎么实现呢?
显然我们可以直接通过实现InputStream来实现这几个子类,用于表示这几个功能
但是就又出现了一个问题,如果既想要 支持基本数据类型,又想具有缓冲的功能怎么办? 如果还用继承的想法会出现什么问题?
那就又回到了组合的问题上来了,4种功能就会出现4*3*2*1=24 中组合,类的个数直接爆炸了.....
回想下我们之前想到过的设计模式---> 装饰器模式
就可以完美的解决这个问题,装饰器模式是继承的一种良好替代方式,能过有效的避免类的个数的爆炸问题
并且还能够动态的增加或者减少功能
看下UML图
通过UML图可以看得到,我们还需要一个Decorator类,我们的具体的装饰类个数不止一个,显然不能省略这个Decorator抽象类
(不清楚装饰器模式的没办法理解这种逻辑,请务必看明白装饰器模式)
这个Decorator就是我们的FilterInputStream (java.io)
看下类图,黑色部分为装饰器模式的角色 节点流表示上面说到的节点流 ByteArrayInputStream/FileInputStream/PipedInputStream/ObjectInputStream/ FilterInputStream中包含一个InputStream属性(是你还有你) |
下面我们看下InputStream下的类继承体系
现在你是否已经可以大致的明白,这些类都是做什么的了呢?
另外还有一些不在java.io包中的类
这些不是IO主体系内的东西,但是依赖于IO ,从事着跟IO相关的一些工作,所以也扩展自InputStream
后面将会单独进行介绍,此处不展开讨论
SocketInputStream (java.net)
CheckedInputStream (java.util.zip)
DeflaterInputStream (java.util.zip)
GZIPInputStream (java.util.zip)
InflaterInputStream (java.util.zip)
ZipInputStream (java.util.zip)
JarInputStream (java.util.jar)
|
2.2.2 OutputStream
2.2.2.1 OutputStream节点流
数据源与OutputStream的结合
字节数组 | ByteArrayOutputStream (java.io) |
文件 |
FileOutputStream (java.io)
|
管道 |
PipedOutputStream (java.io)
|
对象 | ObjectOutputStream (java.io) |
仍旧是数据源与OutputStream的组合
ByteArrayOutputStream |
其中的数据被写入一个 byte 数组
缓冲区会随着数据的不断写入而自动增长, 可使用 toByteArray() 和 toString() 获取数据
关闭 ByteArrayOutputStream 无效
此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException
|
ByteArrayOutputStream() ByteArrayOutputStream(int size) 无参会调用有参,设置默认值 |
FileOutputStream | 信息写入文件 | FileOutputStream(String name) FileOutputStream(String name, boolean append) FileOutputStream(File file) FileOutputStream(File file, boolean append) FileOutputStream(FileDescriptor fdObj) 与FileInputStream几乎一样,不同的是第二个参数用于设置是否是append追加 |
PipedOutputStream | 可以将管道输出流连接到管道输入流来创建通信管道 管道输出流是管道的发送端 通常,数据由某个线程写入 PipedOutputStream 对象 并由其他线程从连接的 PipedInputStream 读取 不建议对这两个对象尝试使用单个线程,因为这样可能会造成该线程死锁 |
PipedOutputStream(PipedInputStream snk) PipedOutputStream() |
ObjectOutputStream | ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream 可以使用 ObjectInputStream 读取(重构)对象 通过在流中使用文件可以实现对象的持久存储 如果流是网络套接字流,则可以在另一台主机上或另一个进程中重构对象 |
ObjectOutputStream(OutputStream out) ObjectOutputStream() |
2.2.2.2 OutputStream过滤流
类似InputStream,OutputStream也需要有支撑基本数据类型的功能,以及缓冲的功能
另外,既然是输出,还希望能够输出各种类型的数据,这样子将会更加方便
也就是
基本数据类型支持/缓冲/便捷输出 |
DataOutputStream (java.io)
BufferedOutputStream (java.io)
PrintStream (java.io)
|
同InputStream 一样
扩展的功能,类库设计者依然是使用装饰器模式
FilterOutputStream (java.io) 是我们的Decorator
完整的家谱
非IO包中的,但是却跟IO相关的一些功能点,跟OutputStream相关的类 |
SocketOutputStream (java.net)
CheckedOutputStream (java.util.zip)
DeflaterOutputStream (java.util.zip)
GZIPOutputStream (java.util.zip)
InflaterOutputStream (java.util.zip)
JarOutputStream (java.util.jar)
ZipOutputStream (java.util.zip)
|
扩展的家谱
2.2.3 Reader
2.2.3.1 Reader节点流
数据源与Reader的结合
字符数组 | CharArrayReader (java.io) |
String | StringReader (java.io) |
文件 |
FileReader (java.io)
|
管道 |
PipedReader (java.io)
|
CharArrayReader | 实现一个可用作字符输入流的字符缓冲区 | CharArrayReader(char buf[]) CharArrayReader(char buf[], int offset, int length) |
StringReader | 其源为一个字符串的字符流 | StringReader(String s) |
FileReader | 用来读取字符文件的便捷类 | FileReader(String fileName) FileReader(File file) FileReader(FileDescriptor fd) |
PipedReader | 管道字符输入流 | PipedReader(PipedWriter src) PipedReader(PipedWriter src, int pipeSize) PipedReader() PipedReader(int pipeSize) |
字节和字符作为数据的存储单位,自然经常有转换的需要
InputStreamReader 就是InputStream 转换为Reader的类
InputStreamReader | 转换为Reader InputStreamReader 是字节流通向字符流的桥梁 它使用指定的 charset 读取字节并将其解码为字符 它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集 每次调用 InputStreamReader 中的一个 read() 方法都会导致从底层输入流读取一个或多个字节 为了达到最高效率,可要考虑在 BufferedReader 内包装 InputStreamReader |
InputStreamReader(InputStream in) InputStreamReader(InputStream in, String charsetName) InputStreamReader(InputStream in, Charset cs) InputStreamReader(InputStream in, CharsetDecoder dec) 构造方法很清晰,接受一个InputStream 并且可以自定义字符编码 |
对于类的转换,设计模式中使用了适配器模式
通过构造方法接收InputStream,然后通过内部的StreamDecoder处理
StreamDecoder 和 StreamEncoder 是作为字符输入和输出转换的关键类,后续有时间会介绍到
属于适配器模式中的对象适配器模式
Reader 是Target
InputStream 是 被适配者 Adaptee
InputStreamReader 是 Adapter
|
需要注意的是,FileReader 与字节流中的FileInputStream 和 FileOutputStream 也是不一样的
FileReader 继承 InputStreamReader
2.2.3.2 Reader过滤流
字符流Reader也依然有装饰器模式的应用
BufferedReader (java.io)
LineNumberReader (java.io)
PushbackReader (java.io)
|
不过需要注意,Reader字符流的装饰器模式应用跟字节流的有些差别
在字节流中,扩展功能都是通过FilterInputStream 或者 FilterOutputStream
然而,在我们的Reader中
BufferedReader 和 FilterReader 各自是一个装饰器模式
Reader家族完整的族谱
2.2.4 Writer
数据源与writer的结合
字符数组 | CharArrayWriter (java.io) |
String | StringWriter (java.io) |
文件 |
FileWriter (java.io)
|
管道 |
PipedWriter (java.io)
|
CharArrayWriter |
实现一个可用作 Writer 的字符缓冲区
缓冲区会随向流中写入数据而自动增长 可使用 toCharArray() 和 toString() 获取数据。
在此类上调用 close() 无效
并且在关闭该流后可以调用此类中的各个方法,而不会产生任何 IOException
|
CharArrayWriter() CharArrayWriter(int initialSize) 内部包含char buf[] size为大小 构造方法用来初始化缓冲区 |
StringWriter | 将输出收集到一个字符缓冲区 StringBuffer的字符流,可以用来构造字符串 关闭 StringWriter 无效 此类中的方法在关闭该流后仍可被调用,而不会产生任何 IOException |
StringWriter() StringWriter(int initialSize) 构造方法初始化缓冲区 |
FileWriter | 用来写入字符文件的便捷类 类似FileReader继承自InputStreamReader 他继承自OutputStreamWriter |
FileWriter(String fileName) FileWriter(String fileName, boolean append) FileWriter(File file) FileWriter(File file, boolean append) FileWriter(FileDescriptor fd) 构造方法都是用来设置文件 |
PipedWriter | 管道字符流 | PipedWriter(PipedReader snk) PipedWriter() |
转换流
OutputStreamWriter | 类似InputStreamReader 作为转换器使用 OutputStreamWriter 是字符流通向字节流的桥梁 可使用指定的 charset 将要写入流中的字符编码成字节 使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集 每次调用 write() 方法都会导致在给定字符(或字符集)上调用编码转换器 为了获得最高效率,可考虑将 OutputStreamWriter 包装到 BufferedWriter 中
例如:
Writer out = new BufferedWriter(new OutputStreamWriter(System.out));
|
OutputStreamWriter(OutputStream out, String charsetName) OutputStreamWriter(OutputStream out) OutputStreamWriter(OutputStream out, Charset cs) OutputStreamWriter(OutputStream out, CharsetEncoder enc) 获取OutputStream然后进行转换,或者指定具体的字符编码 |
FilterWriter | 类似其他的Filter类,作为装饰器模式的Decoder角色 以便具体的装饰器角色可以使用 |
BufferedWriter 以及 PrintWriter类似Reader 不同于字节流的装饰器模式应用
他们都自成一个模式的应用
他俩都单独是Writer 也都包含一个Writer
Writer下完整的家谱
2.3 IO类层次结构总结
前面已经对IO类的基本层次结构进行了一个逻辑上的概述
我们现在归纳概括下一些基本特点
IO的逻辑功能设计点 由 数据源,流的方向,流的数据形式三部分组合而成,这个组合构成了IO的基本功能 另外还有扩展功能,扩展功能以基础功能作为依托,底层依赖基本功能 每种形式的基本功能和扩展功能构成了该形式的功能的集合 |
数据源形式比较多,但是对于流的数据形式以及流的方向是固定的 所以所有的类的基础,都是基于 流的数据形式以及流的方向的组合 也就是 字节输入 字节输出 字符输入 字符输出 这四个形式是固定的 分别使用 InputStream OutputStream Reader Writer来表示这四大家族 前面两个表示字节 后面两个表示字符 |
绝大多数的扩展都以 上面四个名词作为后缀,表示是他的家族成员 |
基本功能对于字节涉及下面几个关键词 ByteArray File Piped Object 扩展功能对于字节涉及涉及下面几个关键词
Data Buffered Pushback LineNumber print
|
基本功能对于字符涉及涉及下面几个关键词
CharArray String File Piped
扩展功能对于字符涉及涉及下面几个关键词 Buffered Print |
虽然四大家族都由基本功能以及扩展功能组成 但是字符和字节的实现形式却并不完全相同 字节流的扩展功能比较依赖装饰器角色FilterInputStream 以及 FilterOutputStream 但是字符流的扩展功能不完全依赖FilterReader 以及 FilterWriter |
数据源与四大家族的结合组合成了基本功能 也就是节点流 扩展功能点与四大家族的结合组成了扩展功能 也就是过滤流 |
另外还有几个工具一样的存在 SequenceInputStream 用于合并InputStream InputStreamReader 以及OutputStreamWriter 用于转换 使用了适配器模式 |
本文主要是从逻辑上介绍了IO家族,虽然实现上都略有差异
但是基本的命名习惯和功能点四个家族是非常类似的
只有理解了类库的逻辑出发点,才能理解IO整个的类库,而本文正是从逻辑上去解读类库的设计