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面试题可以参考参考下面这篇文章

 

 

 

 

 

 

 

posted @ 2020-05-30 19:36  litterCoder  阅读(337)  评论(0编辑  收藏  举报