Java IO 学习笔记(一)
一、什么是流
任何程序,都应该有输入输出的方式。我们难以想象,无法进行输入和输出的程序,能有什么作用。因为这时候的它,与外界是无法联系的。
比方说,我们的 web 服务器。它需要从用户那边读取用户的请求信息,然后再根据请求信息选择向用户响应些什么。如果 web 程序没有输入输出的方式,那上述的请求与响应自然也就无法实现了。
因此,任何编程语言,都需要为开发人员提供输入和输出的方式,而这种输入和输出,就是基于流(Stream)实现的。
流,看起来像是一个挺抽象的概念,但如果你把现实中的溪流与它作比较的话,又会觉得它很形象。它其实就是一条连接程序和外界的“溪流”。当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件、内存或是网络连接;类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候,数据就好比溪流中的水,在流中“流动”。
基于以上,我们可以这么说,流,是程序输入与输出的方式。
二、Java IO
Java IO 实际上就是 Java 语言以流为基础实现数据输入与输出功能的类库。 在 Java IO 中,所有的数据都会被串行化写入输出流或者是从输入流读入。
要全面地学习 Java IO 不是一件容易的事,因为这是一个内容十分庞大的类库,涉及到非常多领域的数据传输。
这里需要指出一点,很多新手在理解 Java IO 的输出与输入时,会将其局限在程序与文件系统的交互中。然而,文件的读写,只能算是 Java IO 中的一部分,实际上,Java IO 还涉及到网络通信、对象传输等等一系列领域。
三、流的分类
- 按流向分:
- 输入流:程序可以从中读取数据的流。
- 输出流:程序能向其中写入数据的流。
- 按传输单位分:
- 字节流:以字节为单位传输数据的流。
- 字符流:以字符为单位传输数据的流。
输入流和输出流是比较好理解的,这里不再赘述。但是对于新人来说,可能容易把字节流和字符流混淆,甚至是压根不知道这两者具体指的是什么,因此,这里我们需要再介绍一些基础概念。
- 比特(Bit): binary digit(二进制数位)的缩写,又称“位”,是计算机信息量的最小单位,同时也是二进制数字中的位。
- 字节(Binary): 计算机中信息计量的一种单位,1 个字节由 8 个位组成,例如:00000001。
- 字符(Character): 文字与符号的总称,可以是各个国家的文字、标点符号、图形符号、数字等。
有了这些基础概念做铺垫后,我们再来谈谈字节流和字符流。
从字面上我们就可以理解,字节流就是以字节为单位处理数据的流。它只能读取或者输出字节,一组由 8 个位(Bit)组成的数据(如 00000001 等),也就是计算机所擅长识别的数据。如果你想把我们人类所能理解的字符交给它传输,很抱歉,它做不到。
那么,一个无法识别人类语言的程序,有什么作用呢?因此,就有了字符流的诞生。
顾名思义,字符流就是用来传输字符的流。它可以将计算机中的二进制数据进行处理后,转化成我们人类所能识别的字符。但正是字符流的这种作用,才会给我们初学 IO 时带来种种困难。因为它涉及到了计算机语言和人类语言的相互转化,也就是我们常提到的编码与解码。事实表明,实际开发中频繁出现的乱码问题,其根源就在于编码与解码的不一致。
讲到这里,如果还是不能理解字节流和字符流的差别的话,也不用着急,因为日后开发中踩到的乱码坑多了,自然也就能理解了。(一脸坏笑)
当然,如果对编码与解码有兴趣的朋友,不妨阅读此文:字符编码的前世今生
四、基本接口
Java IO 有两组基本接口,一组基于字节,另一组基于字符。在本文中,我们先介绍基于字节的操作接口,分别是 InputStream 和 OutputStream。至于字符流的操作接口,我们将在之后的学习笔记中进行总结。
4.1 InputStream
InputStream 是基于字节的输入流。它本身是一个抽象类,具体的实现由子类负责。
如图所示,在 InputStream 中,包含以下方法:
这些方法的具体作用,请见下面的表格:
方法名 | 作用 |
---|---|
read(byte[] b) | 从流中读取一组字节,并将其保存在一个字节数组中。若该字节数组的长度为 0,则函数不读取任何字节,并返回 0;若流中已经没有字节可以读取,则函数返回 -1。 |
read(byte[] b, int off, int len) | 从流中的某个位置读取指定长度的字节,并保存到字节数组的相同位置中。其中,第一个 int 类型参数代表开始读取的位置,第二个 int 参数代表准备读取的长度。 |
skip(long n) | 允许程序跳过流中的 n 个字节。 |
available() | 返回流中剩余的可读字节数。 |
close() | 关闭流,并释放与该流有关联的系统资源。 |
mark(int readlimit) | 允许程序在读取流的过程中,记录指定的位置,通常配合 reset 方法使用。 |
reset() | 允许程序返回到上次在流中所标记的位置,通常配合 mark 方法使用。 |
markSupported() | 判断当前流是否支持 mark 和 reset。 |
这里我们少描述了一个方法,那就是 read()。这是为什么呢?首先,先让我们看看这个函数的完整说明。
/**
* Reads the next byte of data from the input stream. The value byte is
* returned as an <code>int</code> in the range <code>0</code> to
* <code>255</code>. If no byte is available because the end of the stream
* has been reached, the value <code>-1</code> is returned. This method
* blocks until input data is available, the end of the stream is detected,
* or an exception is thrown.
*
* <p> A subclass must provide an implementation of this method.
*
* @return the next byte of data, or <code>-1</code> if the end of the
* stream is reached.
* @exception IOException if an I/O error occurs.
*/
public abstract int read() throws IOException;
这段说明的大致意思是,read() 方法的作用是从流中读取下一个字节的值,并返回该字节的十进制结果(我们知道,一个字节是由 8 个 bit 组成,因此其范围是 00000000 ~ 11111111,换算成十进制就是从 0 到 255)。
但是我们也注意到,这个方法是一个抽象方法,它需要继承它的子类来完成具体的实现。如果这个方法没有被实现,那么我们上述表格中阐述的所有方法将没有任何意义。这也是我将这个方法拿出来单独介绍的原因。
4.2 OutputStream
OutputStream 是基于字节的输出流。它同样也是一个抽象类。
如图所示,在 OutputStream 中,包含以下方法:
很简单有木有!比 InputStream 少多了!
同样,这些方法的作用,可见下面表格:
方法名 | 作用 |
---|---|
write(int b) | 将某个字节,写到输出流中。需要注意的是,该方法同 read() 方法一样,也是抽象方法。 |
write(byte[] b) | 将一个字节数组,写到输出流中。 |
write(byte[] b, int off, int len) | 从字节数组的某个位置开始,输出指定长度的字节数到输出流中。 |
flush() | 清理输出流中的缓存,同时强制输出流中的所有缓存字节。 |
close() | 关闭流,并释放与该流有关联的系统资源。 |
五、类层次结构
InputStream 和 OutputStream 只是 Java IO 中的基础接口,两个接口下还有着丰富的实现类。下面,将分别介绍两个基本接口的类层次结构。(注:InputStream 和 OutputStream 的类层次结构在不同版本的 JDK 中会有所不同,本文是以 JDK 6 为基础的)
5.1 InputStream
在 InputStream 的体系中,会根据实现方式和处理的数据类型划分为不同的子类。尽管类别很多,但对于日常的开发而言,只要重点掌握 FileInputStream 和 FilterInputStream 即可。
FileInputStream: 顾名思义,FileInputStream 是用于从文件系统中读取字节的输入流。这里需要强调的是,它只能以字节的方式读取文件中的数据,而无法以字符的方式。无论用户在显示器上看到的是何种字符,通过 FileInputStream 读取到的也只是二进制数据的十进制表示。例如,文本中有一个字符 ‘a’,此时通过 FileInputStream 读取到的值是 97,转换成二进制数据则为 01100001,即 ‘a’ 这个字符在 ASCII 码表中对应的二进制数值。
FilterInputStream: 中文名为过滤流,是为了某种特定目的而过滤字节的数据流。例如,BufferedInputStream 在 InputStream 的基础上提供了缓冲区;再比如,DataInputStream 为整数、浮点数、字符串等的读取提供了可能。
5.2 OutputStream
输出流的类层次结构与输入流大体上是一致的。在 OutputStream 中,需要重点掌握的同样是 FileOutputStream 和 FilterOutputStream 两个子类。其具体作用不再说明,可参考输入流中的介绍。
写在最后:本篇只是个人在 Java IO 学习过程中的第一篇笔记,主要是从整体介绍一下 Java IO 的一些主要概念,在下一篇文章中,将针对 FileInputStream 和 FileOutputStream 展开详细的讨论。