Java IO --- 输入/输出流
一.概念
1.1 IO流的分类
①根据数据流向分为:输入流 和 输出流
②根据处理数据不同分为:字节流 和 字符流
③根据功能不同分为:节点流 和 处理流 (装饰者模式的体现)
节点流:程序直接操作目标所对应的类叫做节点流。
处理流:程序通过间接流去调用节点流类操作数据,而自己则增加一些额外的功能,这就是处理流。因为处理流不能够直接获取到数据,所以构建一个处理流时,需要在构造函数中传入一个节点流。
由于字符流和字节流大部分功能相似,本文详细讲解一种就够了,以字节流为例子。
1.2 IO流的概念
输入流(InputStream):可以从其中读取一个字节序列的对象称作输入流
输出流(OutputStream):可以向其中写入一个字节序列的对象称作输入流
这些字节序列的来源地和目的地可以是文件,而且通常是文件,但是也可以是网络连接,甚至可以是内存块。
1.3 IO流的读写功能
①InputStream类有一个抽象方法:
public abstract int read() throws IOException;
这个方法将读入一个字节,并返回读入的字节,或者遇到输入源结尾时返回 -1。在设计一个具体的输入流的时候,必须要覆盖这个方法并提供适用的功能。
InputStream类还有若干非抽象的方法,他们可以读入一个字节数组,或者跳过大量的字节。这些方法最终都会调用到抽象的read方法,因此,各个子类只需要覆盖这一个方法。
②与此类似,OutputStream定义了下面的抽象方法:
public abstract void write(int b) throws IOException;
它可以向某个输出位置写出一个字节。
③read 和 write方法在执行的时候都将阻塞,直至字节确实被读入或写出。这就意味着,如果当前的流不能被立刻访问(通常是因为网络连接忙),那么当前的线程将会阻塞。其他的线程可以继续执行有用的工作。
④InputStream提供了available方法使我们可以去检查当前可读入的字节数量,这样意味着像下面这样的代码就不可能被阻塞。
1 int available = inputStream.available(); 2 if (available > 0){ 3 byte[] data = new byte[available]; 4 inputStream.read(data); 5 }
⑤当你完成对输入/输出流的读写时,应该通过调用close方法来关闭它。这个调用将会释放掉十分有限的系统资源。如果一个程序打开了过多的输入/输出流而没有关闭,那么系统资源将被耗尽。
⑥关闭一个输出流的同时还会冲刷该输出流的缓存区,因为输出流为了保证写入的效率,通常会构建一个缓存区,当我们调用write时,会将数据写入缓存区,当缓存区的数据达到了一定的量级后,才会真正被传递出去。所以如果我们不关闭一个输出流,那么缓存区中的最后一个包可能永远也得不到传递。当然,我们也可以调用flush方法,手动将缓存区中的包全部刷上去。
1.4 API
1.4.1 InputStream
① abstract int read()
从数据中读取一个字节,并返回该字节。如果遇到了输入流的末尾,则返回 -1
② int read(byte b[])
将数据读入一个字节数组中,并返回实际读到的字节数。如果遇到了输入流的末尾,则返回 -1。
该方法最多读入 b.length个字节。
③ int read(byte b[], int off, int len)
将数据读入一个字节数组中,并返回实际读到的字节数,如果遇到输入流的末尾,则返回 -1。
参数:b 数据读入的数组
off 第一个读入的字节应该放置的位置在b中的偏移量
len 读入字节的最大数量
④ long skip(long n)
在输入流中跳过n个字节,返回实际跳过的字节数(如果碰到了输入流的末尾,则可能小于n)
⑤ int available()
返回在不阻塞的情况下可获取的字节数量(回忆一下,阻塞意味着当前线程失去对它资源的占用)
⑥ void close()
关闭这个输入流
⑦ void mark(int readlimit)
在输入流的当前位置打个标记(并非所有流都支持这个特性)。如果从输入流中读入的字节已经多于readlimit个,那么这个流允许忽略这个标记。
⑧ void reset()
返回到最后一个标记,随后对read的调用将重新读入这个字节。如果当前没有任何标记,则这个流不被重置。
⑨ boolean markSupported()
如果这个流支持打标记,则返回true。
1.4.2 OutputStream
① abstract void write(int b)
写出一个字节的数据。
② void write(byte b[])
③ void write(byte b[], int off, int len)
从字节数组b中写出所有的字节或者某个范围的字节到流中
参数:b用于写出数据的数组
off第一个被写出的字节在b中的偏移量
len 写出字节的最大数量
④ void flush()
冲刷输出流,也就是将所有缓冲的数据发送到目的地
⑤ void close()
冲刷并关闭输出流。
二.IO流的庞大家族
由于IO流的家族体系十分庞大,这里列举几个比较重要的,经常用到的。
2.1 InputStream
看到上面这个图片,如果大家熟悉装饰者模式的话,就会发现这个结果就是装饰者模式。而确实,Java IO就是装饰者模式的最好的实例。(黄色部分就是上文所说的节点流,绿色部分则是处理流)
通过上图我们可以看到,InputStream是所有输入流的父类,它定义了输入流的基本方法,并且定义了抽象方法 read,需要具体的子类去实现如何从源中读取一个字节(对应了装饰者模式中的 抽象构件)。
而上图中的黄色部分,都是 InputStream的子类,这些子类都分别实现了对应的 read方法(对应了装饰者模式中的 具体构件)。
而上图中的FilterInputStream虽然也是InputStream的子类,但是它其中的成员变量又包含了一个InputStream,它的read方法,实际上是调用传入的InputStream的read (对应了装饰者模式中的 装饰角色)。
而FilterInputStream的子类,则会增加一些额外的功能,它们不关心如何读取数据,只会关心读取数据后做一些额外的处理(对应了装饰者模式中的 具体装饰角色)。ObjectInputStream也是一个具体的装饰角色。
不了解装饰者模式的朋友,可以先了解一下,这样易于理解。
Java 使用了装饰者模式将获取数据内容 与 对数据的处理分离开来,具体使用的时候我们就可以根据自己的需求组合使用,如下:
我们想要从一个文件中,读取一个Double类型的数据,我们就需要组合 FileInputstream(从文件中读取字节)和DataInputStream(将字节数组封装成其他数据对象)。
1 FileInputStream fin = new FileInputStream("test.txt"); 2 DataInputStream din = new DataInputStream(fin); 3 double x = din.readDouble();
2.2 OutputStream
OutputStream的类图关系与InputStream类似,这里就不过多阐述了。
2.3 Reader (字符输入流)
Reader相当于InputStream,只不过一个是字节输入流的父类,一个是字符输入流的父类。
另外看到网上不少博客都将FileReader定义为 节点流,我认为这是不正确的。FileReader只实现了三个构造函数,都是构造一个FileInputStream传给自己父类 InputStreamReader,然后就没做其他的处理了,相当于就是扩展了一下父类InputStreamReader 的构造函数而已,而它的父类InputStreamReader 就是一个处理流,那么FileReader也应当是一个处理流。
2.4 Writer (字符输出流)
Writer相当于OutputStream,只不过一个是字节输出流的父类,一个是字符输出流的父类。
三.常见问题
Java面试中常见的一些IO面试题可以参考参考下面这篇文章