目录
  1. 1. IO框架
  2. 2. IO简介
    1. 2.1. Java I/O三个层次介绍
    2. 2.2. IO在Android平台应用及其原理
      1. 2.2.1. 设计模式 - 装饰器模式
      2. 2.2.2. 字节流
        1. 2.2.2.1. 字节流功能分类
          1. 2.2.2.1.1. FilterOutputStream
          2. 2.2.2.1.2. BufferedOutputStream
      3. 2.2.3. 字符流
        1. 2.2.3.1. 字符流功能分类
          1. 2.2.3.1.1. FilterWriter/FilterReader
          2. 2.2.3.1.2. BufferedWriter/BufferedReader
          3. 2.2.3.1.3. OutputStreamWriter/InputStreamReader
          4. 2.2.3.1.4. FileReader/FileWriter
      4. 2.2.4. 字节流与字符流
        1. 2.2.4.1. 两者区别
        2. 2.2.4.2. 两者转换
重拾Android-Java进阶之深入理解IO

第九天 深入剖析I/O流

IO框架

Java IO学习是一件非常艰巨的任务,这挑战来自于其覆盖了所有的技术可能性。

它不仅存在各种I/O源端还有想要和它通信的接收端(文件/console/网络),而且还需要以不同的方式与它们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等),这些综合起来就需要我们开发人员去大量的学习和知识技能补充。

当然要学会这些Java的I/O是比较难的,因为这其实需要我们去构建一个源于IO的知识体系。要构建这个体系又需要我们深入理解IO库的演进过程。那么接下来就开始学习吧。

IO简介

数据流是一组有序、有起点和终点的字节的数据序列。包括输入流和输出流。

流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种:
1)字节流: 数据流中最小的数据单元是字节
2)字符流: 数据流中最小的数据单元是字符

Java.io包中最重要的就是5个类和一个接口。5个类指的是 File、OutputStream、InputStream、Writer、Reader;一个接口指的是 Serializable。

Java I/O三个层次介绍

  1. 流式部分 — 最主要的部分。如:OutputStream、InputStream、Writer、Reader等
  2. 非流式部分 — 如 File类、RandomAccess类和FileDescriptor等类
  3. 其他 — 文件读取部分的安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。

图片.png

IO在Android平台应用及其原理

在Android平台,从应用的角度出发,我们最需要的关注和研究的就是 字节流(Stream)和 字符流(Reader/Writer)以及 File/RandomAccessFile。在介绍这些流的读取与写入之前,首先必须掌握的,就是设计模式中的修饰模式,学会并理解修饰模式是搞懂流的必备前提条件。

设计模式 - 装饰器模式

字节流

从流的整个发展史来看出现的各种类之间的关系,都是沿用了装饰器模式,都是一个类的功能可以用来修饰其他类,然后组合成一个比较复杂的流,比如说:

DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(
new File(file)
)
)
);

从上面的代码块中不难看出这些类的关系:为了向文件中写入数据,首先需要创建一个FileOutputStream,然后为了提升访问的效率,将它发送给具备缓存功能的BufferedOutputStream,而为了实现与机器类型无关的Java基本类型数据的输出,所以,我们将缓存的流传递给了 DateOutputStream。从上面的关系我们可以看到,其基本目的都是为了OutputStream添加额外的功能。而这种额外的功能的添加就是采用了装饰模式来构建的代码。

下面的图是一个关于字节流知识体系的图谱,这张图谱比较全面的概况了我们字节流中间的各个类以及它们之间的关系。

图片.png

字节流功能分类

首先明确一点,我们按照一个学习路线来学习能很好的理解字节流功能,那为什么要这样做呢?原因是它们的功能决定的。

OutputStream -> FileOutputStream/FilterOutputStream -> DataOutputStream -> BufferedOutputStream

相应的学习InputStream方法即可。

这里必须强调:所有的写入写出,其参考的对象都是内存,从内存写入文件,从文件读取到内存,这一点搞清楚,才能理解输入输出的奥义。

FilterOutputStream

从学习的角度来,我们首先应该掌握FilterOutputStream,以及FileOutputStream,这两个类是基本的类,从继承关系来看,不难发现它们都是对abstract类OutputStream的拓展,都是它的子类。然而,伴随着对Stream流的功能的拓展,所以后面出现了 DataOutputStream(将Java中的基础数据类型写入数据字节输出流中,并保存在存储介质中,然后可以用DataOutputstream从存储介质中读取到程序中还原成Java基础类型)。通过使用装饰器模式,使得避免了类的爆炸式增长。

BufferedOutputStream

为了提升Stream的执行效率,所以出现了BufferedOutputStream。BufferedOutputStream实质上是本地添加了混存的数组。在使用BufferedOutputStream之前每次从磁盘读入数据的时候都是需要访问多少byte数据就向磁盘中读多少个byte的数据,而出现BufferedOutputStream之后,策略就改了,会先读取整个缓存空间相应大小的数据,这样就是从磁盘读取了一块比较大的数据,然后缓存起来,从而减少了对磁盘的访问次数以达到提升性能的目的。

另外一方面,我们知道了OutputStream(输出流)的发展历史后,便可以知道如何使用OutputStream了,同样的方法,也可以运用到InputStream中来。

字符流

下面是一个关于字符流的知识图谱,这张图谱比较全面的概况了我们字符流中间的各个类以及它们之间的关系。

图片.png

字符流和和字节流同样,只是,字节流面向的是我们位置或者即使知道它们的编码格式也意义不大的文件(png、exe、zip)的时候采用字节,而面对一些我们知道的文件构造,我们就能够搞懂它意义的文件(json、xml)等文件的时候我们还是需要以字符的形式来读取,所以就出现了字符流。Reader和Stream最大的区别应该是前者包含了一个 readLine()接口,这个接口就表明,这是一行数据的意义,这也是可以理解的,因为只有字符才具备行的概念,相反字节流中的行也就是一个字节符号而已。

字符流功能分类

Writer -> Filter< -> BufferedWriter -> OutputStreamWriter -> FileWriter -> 其他

FilterWriter/FilterReader

字符过滤输出流与FilterOutputStream功能一样,只是简单重写了父类的方法,目的是为了所有装饰类提供标准和基本的方法,要求子类必须实现核心方法,和拥有自己的专属功能。这里的FilterWriter没有子类,大概其意义只是为了提供一个接口,留待日后拓展,当然其本身也是一个抽象类。

BufferedWriter/BufferedReader

BufferedWriter是Writer类的一个子类。它的功能是为传入的底层字符输出提供缓存功能。同样当使用底层字符输出流向目的地写入字符或者字符数组时,每写入一次就要打开一次与目的地的连接,这样频繁的访问不断降低写入写出效率,也有可能导致对存储介质造成一定的破坏。比如,当我们向磁盘中不断的写入字节时,夸张情况下,将一个非常大的单位(eg. GB)的字节数据写入到磁盘的指定文件中,每写入一个字节就要打开一次这个磁盘的通道,这个结果无疑是恐怖的,而当我们使用BufferedWriter将底层字符输出流,比如FileReader包装一下之后,我们便可以在程序中,将要写入到文件中的字符先写入到BufferedWriter的内置缓存中,然后当达到一定数量时,一次性写入FileReader流中,此时FileReader就可以打开一个通道,将这个数据块写入到文件中,这样做虽然不可能达到一次访问就将所有数据写入磁盘中的效果,但也大大的提高了效率和减少了磁盘的访问量。

OutputStreamWriter/InputStreamReader

输入字符转换流,是输入字节流转向输入字符流字符流的桥梁。用于将输入字节流转换成输入字符流,通过指定的或者默认的编码将从底层读取的字节转换成字符返回到程序中,这与OutputStreamWriter一样,本质也是使用其内部的一个类来完成所有的工作:
StreamDecoder - 使用默认或者指定的编码将字节转换成字符;
OutputStreamWriter/InputStreamReader只是对StreamDecoder进行了一层封装,其内部所有方法核心都是调用StreamDecoder来完成的;

OutputStreamWriter、InputStreamReader分别为InputStream、OutputStream的低级输入输出流提供将字节转换成字符的桥梁,它们只是一层装饰而已,其真正的核心为:

OutputStreamWriter中的StreamEncoder:

  1. 使用指定的或者默认的编码集将字符转码为字节;
  2. 调用StreamEncoder自身实现的写入方法将转码后的字节写入到底层字节输出流中。

InputStreamReader中的StreamDecoder:

  1. 使用指定的或者默认的编码集将字节转码为字符;
  2. 调用StreamDecoder自身实现的读取方法将解码后的字符读取到程序中。

在理解这两个流的时候需要注意:

java.io中只有将字节转换成字符的类,并没有将字符转换成字节的类。原因很简单:字符流的存在本来就是为了对字节流进行装饰、加工处理以便更方便的去使用。
在使用这两个流的时候需要注意:由于这两个流要频繁对读取或者写入的字节或者字符进行转码、解码、以及与底层流的源和目的地进行交互,所以使用的时候使用BufferedWriter、BufferedReader进行包装,以达到最高效率和保护存储介质。

FileReader/FileWriter

FileReader和FileWriter继承于InputStreamReader/OutputStreamWriter。从源码可以看出:

FileWriter文件字符输出流,主要用于将字符写入到指定的打开的文件中,其本质是通过传入的文件名、文件、或者文件描述符来创建FileOutputStream,然后使用OutputStreamWriter使用默认编码将FileOutputStream转换成Writer(这个Writer就是FileWriter)。如果考虑使用FileWriter类的话,最好搭配使用BufferedWriter包装一下,效果会很好。

FileReader文件字符输入流,主要用于将文件内容以字符形式读取出来,一般用于读取字符形式的文件内容,也可以读取字节形式,但是因为FileReader内部也是通过传入的参数构造InputStreamReader,并且只能使用默认编码,所以由于其无法控制编码问题,就会导致容易出现乱码。所以在读取字节形式的文件时还是使用字节流来操作会好一点,同样在使用此流的时候用BufferedReader包装一下,也能就此提高读取效率,保护存储介质。

字节流与字符流

那么字节输入流和字符输入流之间的关系是怎样的呢?详见下图:

图片.png

同样的,字节输出流和字符输出流的关系也有:

图片.png

两者区别

字节流和字符流的在使用上两者很相似,那么它们的区别在哪?

字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作有关的,而字符流在操作的时候是使用到缓冲区的;字节流在操作文件时,即使不关闭资源(close方法),文件也能输出,但是如果字符流不使用close方法的话,则不会输出任何内容。说明字符流用的是缓冲区,并且可以使用flush方法强制进行刷新缓冲区,这样操作之后才能在不close的情况下输出内容。

那么问题来了,在开发过程中,究竟是使用字节流好还是字符流好呢?

在所有的硬盘上保存文件或者进行传输的时候都是以字节的方法进行的,包括图片也是按字节完成的。而字符是只有在内存中才会形成,所以使用字节的操作是最多的。

如果Java程序实现一个拷贝功能,则应该选用字节流进行操作(可能拷贝图片),并且采用边读边写的方式(节省内存)。

两者转换

虽然Java支持字节流和字符流,但有时需要在字节流和字符流两者之间做转换。InputStreamReader 和 OutputStreamWriter,这两个类就是作为字节流和字符流之间相互转换的类。

InputStreamReader用于将一个字节流中的字节解码成字符:

有两个构造方法:

InputStreamReader(InputStream in);

功能: 用默认字符集创建一个InputStreamReader对象

InputStreamReader(InputStream in, String CharsetName);

功能: 接收已指定字符集名的字符串,并用该字符创建对象

OutputStream用于将写入的字符编码成字节后写入一个字节流:

同样有两个构造方法:

OutputStreamWriter(OutputStream out);

功能: 用默认字符集创建一个OutputStreamWriter对象;

OutputStreamWriter(OutputStream out, String CharsetName);

功能: 接收已指定字符集名的字符串,并用该字符创建OutputStreamWriter对象

为了避免频繁的转换字节流和字符流,对以上两个类进行了封装:

BufferedWriter类封装了OutputStreamWriter类;
BufferedReader类封装了InputStreamReader类;

代码示例:

BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

利用下面的语句,可以从控制台读取一行字符串:

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String line = in.readLine();
打赏
  • 微信
  • 支付宝

评论