java io 源码研究记录(一)
Java IO 源码研究:
一、输入流
1 基类 InputStream
简介:
这是Java中所有输入流的基类,它是一个抽象类,下面我们简单来了解一下它的基本方法和抽象方法。
基本方法:
public void mark(int readLimit) |
标记方法,标准做法是将当前输入流的位置标记下来,等待在某一时刻回到这个位置(当调用了reset方法时) |
public boolean markSupported() |
判断 mark 方法是否可用,若返回 false 则 mark 和 reset 的方法都不可用,默认不支持 |
public void reset() |
重置当前位置到 mark 位置 |
public int read(byte[] b) |
读取 b.length 个字节并将这些字节存放到 b 数组,它调用下面的方法 |
public int read(byte[] b, int off, int len) |
从数组 b 的 off 下标开始读取 len 个字节并存储,返回成功读取到的字节长度,否则返回 -1 |
public int available() |
返回该输入流中还有多少个字节是可以被读取的有效字符,默认返回 0 |
public int skip(long n) |
跳过 n 个字节,默认通过读取 n 个字节来跳过这 n 个字节 |
抽象方法:
public int read() |
读取一个字节并返回该字节,如果读取失败返回 -1 。 抽象方法,依赖于子类的实现,同时也是最基本的方法,它读取一个字节的数据,并且由上层 read(byte[] b, int off, int len)调用 |
个人看法:
首先,了解一下它为什么要叫输入流,原因是这个输入流是相对你的程序而言的,输入流里面的数据,它是要进入你的程序里面的,所以它叫输入流,那么相对输入流的另一端而言,它应该叫输出流。
这是所有输入流的基类,应该要对它的一些基本方法和抽象方法的作用和规则有所了解,因为,每个子类都可以重写这些方法,虽然基类提供了一些基本方法,但是子类可能会由于各种原因,例如效率、用途等覆盖基类的某些基本方法。
1.1 FileInputStream
简介:
这是一个文件输入流,顾名思义,就是数据从文件中读取出来,在文件这端它是输出流,但我们是以你的程序为原点的,所以对程序而言,它是输入流。它是一个和磁盘文件打交道的输入流,通过它,我们就可以读取磁盘中的任意文件,当然了,由于它读的时候是以字节为单位的,所以可能由于各种编码和格式的原因导致输出乱码。顺便一提它不支持 mark 和 reset 方法。
构造方法:
FileInputStream(String fileName); |
通过传入文件的指定路径来获取这个文件的输出流,实际是调用了下面的构造方法 |
FileInputStream(File fileName); |
通过传入一个文件对象引用并打开连接来获取这个文件的输出流 |
FileInputStream(FileDescriptor fdObj); |
通过传入一个文件描述对象(已经打开了文件连接)来获取文件的输出流 |
主要属性:
private FileDescriptor fd; |
打开文件的句柄 |
Private final String path; |
文件路径,如果是调用参数为 fdObj 的构造方法则为 null |
Private volatile boolean closed; |
表示文件连接是否已经被关闭 |
基本方法:
public int read() |
覆盖了基类的 read ,调用了一个本地 read0() 方法 |
public int read(byte[] b , int off, int len) |
覆盖了基类,调用了一个本地 readBytes(byte[] b, int off, int len) 方法 |
public int read(byte[] b) |
同上 |
public int available() |
覆盖了基类,调用了一个本地方法 available0() 方法 |
public int skip(long n) |
覆盖了基类,调用了一个本地方法 skip0(long n) 方法 |
public void close() |
覆盖了基类,主要是关闭和这个文件输入流相关的系统资源,例如 FileDescriptor,也调用了一个本地方法 close() 方法 |
public FileDescriptor getFD() |
获取与此输入流相关的文件描述类对象 |
public FileChannel get Channel() |
获取与此输入流相关的且独一无二的 FileChannel 对象 |
个人看法:
这是一个读取文件的输入流,但是大部分是通过调用本地方法来实现的,具体来说,应该是平台相关的,具体实现细节不得而知,但是根据某说法,它会频繁地进行 IO 操作,结合操作系统而言,是比较耗时,尤其是它频繁地只读取一个字节时,理论上耗时巨大,就目前而言,实际耗时也巨大。但不可否认,它提供了一个相对底层的文件交互功能,为后面的优化读取提供了一个基础。
1.2 FilterInputStream
简介:
它把一个不属于它的输入流(我们称它为被代理对象),当作它自己的数据来源,并通过调用被代理对象来实现它自己的应该要做的事,这里类似于代理模式模式。是否支持 mark 和 reset 方法由其被代理对象决定。
构造方法:
public FilterInputStream(InputStream in) |
传入一个实例输入流对象,并作为自己的被代理对象 |
属性:
protected volatile InputStream in; |
表示被代理的输入流实例对象 |
主要方法:
这里的主要方法和基类基本一致,而且覆盖了所有基类方法,并且代码实现都是直接调用被代理对象的对应方法。
个人看法:
这里使用了代理模式,作用是为了能够对这个被代理对象的某些功能或者方法进行加强,例如后面将要谈到的缓冲输入流和数据输入流,代理模式的方法核心仍然是被代理对象的方法,代理只是为了锦上添花,若想了解更多,可以自行查找有关代理设计模式的资料。
1.2.1 BufferedInputStream
简介:
这个输入流叫做缓冲输入流,它继承了上面所说的 FilterInputStream 类,前面提到过,这只是为了锦上添花,那么为什么需要它呢?我们已经了解过了像文件输入流这种,需要频繁进行 IO 等耗时操作的输入流理论上会耗时很大,为了解决这个问题,我们只需要减少那些耗时大的操作即可,那对于输入流而言,不需要频繁 IO 或其他比较耗时的操作的办法就是一次性读取很多的字节,以避免一个一个字节读取的费时行为,那么有人说了,既然是一次性读取很多很多的字节,基类它也有提供嘛,为什么还要做额外的工作呢?那是因为一个一个字节读取仍然是一个有时必须要进行的一个动作,如果直接读取很多很多的字节,我们需要对这些很多很多的字节进行额外的处理,让它可以被一个一个读取其余数据又不至于丢失,如果没有这些缓冲输入流就需要我们自己来做这件事,这不是不可能做到,但无疑却花费了很多额外的时间,所以缓冲输入流应运而生。
缓冲输入流主要是提供了一个内存上的缓冲字节数组,通过对这个缓冲数组的操作,来实现被代理对象的单字节读取和多字节读取。缓冲输入流一次性会读满整个缓冲数组,也就是读取很多很多的字节,缓冲数组默认大小是 8192 (8K)字节,最大是 Integer.MAX_VALUE - 8 ,当超过这个限度时会直接调用被代理对象来进行相应的处理。
构造方法:
public BufferedInputStream(InputStream in) |
传参 in 作为被代理对象,默认缓冲数组大小为 8192 |
public BufferedInputStream(InputStream in, int size) |
传参 in 作为被代理对象,缓冲数组大小设置为参数 size |
主要属性:
protected volatile byte[] buf |
内置的缓冲数组 |
protected int count |
缓冲数组的当前大小 |
protected int pos |
缓冲数组的下一个被要读取的字节下标 |
protected int markpos |
缓冲数组 mark 开始下标,默认为 -1 |
protected int marklimit |
缓冲数组最多可以被 mark 的个数,只能通过调用 mark(int readlimit) 方法来为其赋值,成员变量默认为 0 ;作用是一当 markpos = 0 并且当前缓冲数组大小大于 marklimit 时,mark 动作失效;二当缓冲数组扩充并且 mark = 0 时,控制新缓冲数组的长度等于 marklimit 和 两倍长 两者的最小值。这里的实现类并不像它所说的那样,当 mark 个数大于 marklimit 时就会令 mark 动作失效,事实上只有当缓冲数组被填充时才会保证 mark 个数不大于 marklimit 。 |
主要方法:
private void fill() |
填充,即填满缓冲数组,触发条件是缓冲数组没有空间了(pos >= count)。当没有 mark 过(markpos = -1)时,缓冲数组不会扩充,否则将进行数组扩充,一般是直接扩大两倍,除非还能够在缓冲数组中挤出空间或者 mark 长度大于 marklimit 了或者当缓冲数组超过最大数组长度值 (Integer.MAX_VALUE - 8) 时抛出异常。扩充后的缓冲数组必须保证下标 markpos - pos 的数据保留,同时缓冲数组大小不超过 marklimit 。 |
public int read() |
覆盖父类的 read ,实现为从缓冲数组中读取一个字节,若数组已满,进行填充。 |
private int read1 (byte[] b, int off, int len) |
从缓冲数组中读取 len 个字节并填充在数组 b 中以下标 off 开始的 len 个字节。如果缓冲数组没有空间了,判断 markpos == -1 && len > buf.length 是否为真,调用被代理对象的 read(b, off, len) ,否则填充,这样做的原因是,没有 mark 这个动作发生,表示缓冲数组将被不会被扩充,但是所要求读取的长度又大于当前缓冲数组的大小,故无法完成,也可以这么说,当没有使用过 mark 这个动作并且有调用这个方法和要求读取个数大于当前缓冲数组大小时,代理对象的效率和被代理对象的效率差不多。 |
public int read (byte[] b, int off, int len) |
覆盖父类方法,通过简单的参数验证后,循环调用上面的方法,直到 len 个字节被成功读取。 |
public int skip(long n) |
覆盖父类方法,当缓冲数组没有空间时,判断有没有 mark 动作,直接调用被代理对象的 skip 方法并返回,否则填充。数组空间足够后接着取填充后的空闲空间和传入参数两者之间的最小值 n 并跳过 n 个字节。这里应该注意 skip 并不实际跳过参数 n 个字节,这取决于是否执行过 mark 动作,如果没有,将严格跳过 n 个字节,否则为了保证 reset 动作发生后的可重复性,就不能直接丢弃 n 个字节,而只能象征性地丢弃缓冲数组中 (buf.length - pos) 个字节,为什么说象征性呢?因为这些数据还在缓冲数组中,也就是说只是将 pos 移动到了 buf.length ,通过 reset 方法这些被丢弃的数据又可以被读取到。 |
public int available() |
覆盖父类方法,返回被代理对象的 available 方法返回值加上缓冲数组中的 count - pos 。 |
public void close() |
覆盖父类方法,除了关闭被代理对象,还将被代理对象设置为空,以及清除缓冲数组并设置为 null(此处可能不严谨) |
个人看法:
只有正确理解缓冲输入流的各个细节,才能够做到真正的高效缓冲,在这里,我试着总结一下:
1、被代理对象最好自身不要有缓冲功能,否则多此一举,就像你也可以将缓冲输入流对象作为被代理对象,可意义何在?你给它双重缓冲?这里一点效果也没有。
2、如果机器性能允许,可以让缓冲数组尽量大,这样读取速度更快,缺点是耗内存。
3、关于代理对象和被代理对象的效率差不多,是因为当要读取的字节长度大于等于当前缓冲数组长度并且没有使用过 mark 动作时,代理对象是直接执行被代理对象的方法。
1.2.2 DataInputStream
简介:
这个叫做数据输入流,产生原因是因为普通的 InputStream 只能读取到单个字节,即 byte 数据类型,而无法读取像 int、byte、unsignedbyte、unsignedshort、short、char、long、boolean、float、double等基本数据类型以及UTF编码的数据,所以就有了数据输入流,当然了,它只是根据各种数据类型的特点,例如各种基本数据类型的长度,int 占 4 位字节,long 占 8 个字节,char、short 占 2 个字节,进而读取相应位数的字节,来组成对应的数据类型,这里会产生一个问题,就是事实上这个输入流表示的并不是这个意思,它却这样读了。
构造方法:
public DataInputStream(InputStream in) |
将参数 in 作为被代理对象 |
主要属性:
继承自父类的输入流。
主要方法:
public int readFully(byte[] b, int off, int len) |
效果同 read(byte[] b, int off, int len) |
public final type readType() |
Type 表示对应的基本数据类型 |
个人看法:
这个容易混淆数据,导致数据意义变更,所以在不知文件结构的情况下应该避免使用。
1.3 ByteArrayInputStream
简介:
这是字节数组输入流,顾名思义,它的输出端是一个字节数组,举一反三,FileInputStream 的输出端自然就是磁盘文件了。在它内部会持有这个字节数组的引用 。
构造方法:
public ByteArrayInputStream(byte[] b) |
参数 b 是输出端数组,这个输入流操作这整个字节数组,默认开启从下标 0 开始的 mark 动作 |
public ByteArrayInputStream(byte[] b, int offset, int len) |
参数 b 是输出端数组,但这个输入流只能操作从下标 offset 开始的数据 len 个字节,但最长不大于数组 b 的原始大小,并且默认开启从 offset 开始的 mark 动作 |
主要属性:
protected int pos |
下一个要读取的数据下标 |
protected int mark = 0 |
Mark 动作标记 |
protected int count |
字节数组引用的大小 |
主要方法:
Public int read() |
实现为读取并返回下一个字节,否则返回 -1 |
Public int read(byte[] b, int off, int len) |
检查参数合法性,读取并返回 len 长度的字节数组,返回的数组有效数据不大于输出端数组剩余的有效数据长度 |
Public long skip(long n) |
跳过 n 个字节,前提是 n 小于输出端数组的有效数据长度(否则跳过输出端数组的有效数据长度),以及 n 非负(否则跳 0 个) |
Public int available() |
返回输出端数组的有效数据长度 |
Public void mark(int readlimit) |
直接标记当前下标 pos,即令 mark = pos |
Public void reset() |
直接令当前下标 pos = mark |
Public void close() |
什么也不做 |
个人看法:
这是一个以数组为输出端的输入流。我觉得可以搭配其他输入流使用,毕竟很多的输入流都是支持用字节数组去读取某长度的字节的。
输入流暂时先告一段落,还差管道输入流和对象输入流还未介绍,但是由于其依赖于其对应的输出流,所以将要先来了解输出流,介绍完一些常见的输出流后,我们再集中介绍相互有关联的输出流和输入流。
二、输出流
2 基类 -- OutputStream
简介:
同理,为什么它叫输出流,原因一样,它是以程序端为原点的,即相对程序而言,它的数据是从内存(程序内部)输出到指定位置。它也是一个抽象类,下面具体介绍它的基本方法和抽象方法。
基本方法:
Public void write(byte[] b) |
往指定位置写入数组 b 中的所有数据,默认调用下面的方法 |
Public void write(byte[] b, int off, int len) |
往指定位置写入数组 b 中从下标 off 开始的 len 个字节,默认通过循环调用 write(int b) 实现写入多个数据 |
Public void flush() |
默认什么也不做 |
Public void close() |
同上 |
抽象方法:
Public abstract void write(int b) |
将数据 b 以 byte 类型写入指定位置 |
个人看法:
同上啦,作为所有输出流的基类,它定义了一个输出流该有的样子,所以应该理解并熟悉其实现及规则。
2.1 FileOutputStream
简介:
文件输出流,顾名思义就是将程序在内存中的某些数据输出到磁盘文件中。
构造方法:
Public FileOutputStream(String fileName) |
实质是调用下面的方法,传参 false ,表示覆盖文本输出。 |
Public FileOutputStream(String fileName, boolean append) |
fileName 表示文件路径名,支持绝对和相对路径, append 表示数据是以覆盖方式还是以文本末追加的方式输出,实际是调用同格式的以 file 主导的构造方法。 |
Public FileOutputStream(File fileName) |
实质是调用下面的方法,传参 false ,表示覆盖文本输出 |
Public FileOutputStream(File fileName, boolean append) |
fileName 表示文件实例对象引用, append 表示数据是以覆盖方式还是以文本末追加的方式输出。 |
Public FileOutputStream(FileDescriptor fd) |
以 FileDescriptor 实例对象引用作为文件连接 |
主要属性:
Private final FileDescriptor fd |
与文件相关的 FileDescriptor 实例对象引用 |
Private final boolean append |
是覆盖写入还是追加写入 |
Private FileChannel channel |
|
Private final String path |
文件路径名 |
主要方法:
和文件输入流一样,基本上是调用本地方法写入文件,具有平台相关性的,没有什么特殊的方法,基本上是覆盖实现了基类的基本方法等。
个人看法:
相对基类而言,并没有什么创新的方法,依靠最基本的读操作(包括单字节读取和多字节读取)打开连接和关闭连接等本地方法,实现基类的功能。
2.2 FilterOutputStream
备注:
这个与 FilterInputStream 是同样的原理,这里不再赘述,同样是使用了代理模式。
2.2.1 BufferedOutputStream
简介:
缓冲输出流,内部具有缓冲数组,通过操作这个数组来存储要写入的数据,当数组满了或者其他原因,就将其中的数据写入目的地。
构造方法:
Public BufferedOutputStream(OutputStream out) |
参数 out 作为被代理对象传入缓冲输出流内部,调用下面的构造方法,默认传参 8 K , |
Public BufferedOutputStream(OutputStream out, int size) |
参数 out 作为被代理对象传入缓冲输出流内部,size 表示内部缓冲数组的大小 |
主要属性:
Protected byte[] buf |
内部缓冲数组 |
Protected int count |
缓冲数组内的有效数据数量 |
Protected OutputStream out |
被代理对象 |
主要方法:
Private void flushBuffer() |
将当前缓冲数组中的全部有效数据写入目的地,并致 count = 0 |
Public void write(int b) |
检查缓冲数组是否有空间,将数据 b 写入缓冲数组,否则刷新再写入 |
Public void write(byte[] b, int off, int len) |
若可以写入缓冲数组,则尽量写入缓冲数组,否则不缓存直接写入 |
Public void flush() |
先清空自己的缓冲,再清空被代理对象的缓冲,这里说明它是有考虑将缓冲输出流作为被代理对象的 |
个人看法:
缓冲输入流,减少了 IO 等耗时操作,在数据量相对庞大的基础上才会具有明显的优势。
2.2.2 DataOuputStream
简介:
数据输出流,同 DataInputStream ,它则是提供了将一些基本类型,如int、short、long、char、float、double等写入目的地的,同时简化了 String 类型的写入。
构造方法:
Public DataOutputStream(OutputStream out) |
传入被代理对象,并传参调用父类构造函数 |
主要属性:
Protected int written |
记录了当前一共写入的字节数,最大为 Integer.MAX_VALUE |
主要方法:
Public final int size() |
返回当前一共写入的字节数 |
Public final void writeType(type n) |
写入对应数据类型的数据,type 表示所有的基本数据类型 |
Public final void writeBytes(String s) |
写入字符串,效果同下 |
Public final void writeChars(String s) |
同上 |
个人看法:
一般写入的时候,只写一种数据类型,否则读取出来后容易出错,若是要写入块数据,不推荐使用这个输出流。
2.3 ByteArrayOutputStream
简介:
字节数组输出流,顾名思义,将数据输出到字节数组中,这个数组是内置的。
构造方法:
Public ByteArrayOutputStream() |
调用下面的构造方法,传参 32 |
Public ByteArrayOutputStream(int size) |
创建一个大小为 size 的内置数组,作为数据的输入端 |
主要属性:
Protected byte[] buf |
内置数组,作为数据输入端 |
Protected int count |
记录当前数组的有效数据数量 |
主要方法:
Private void ensureCapacity(int minCapacity) |
确保内置数组的最小大小为 minCapacity ,先直接增大两倍,若还不够,直接令其等于 minCapacity ,但应注意最大不超过 Integer.MAX_VALUE。 |
Public void write(int b) |
直接写入,不够扩充 |
Public void write(byte[] b, int off, int len) |
直接写入,不够扩充 |
Public void writeTo(OutputStream out) |
将此内置数组的有效数据写入其他输出流 |
Public void reset() |
重置内置数组并抛弃所有数据 |
Public byte[] toByteArray() |
以字节数组形式返回此内置数组的所有有效数据 |
Public String toString() |
以字符串形式输出内置数组的有效数据(ASCII码) |
Public int size() |
返回此内置数组的有效数据 |
个人看法:
本次先记录到这里,下次将介绍 PipedInputStream 和 PipedOutputStream ,敬请期待!