一. 面试题及剖析
1. 今日面试题
今天 壹哥 带各位复习一块可能会令初学者比较头疼的内容,起码当时让我很有些头疼的内容,那就是I/O流。为啥I/O流会让很多初学者头疼呢?其实主要是因为I/O流的分类实在是太多了,一会是输入流,一会是输出流,还有字节流、字符流、文件输入流,文件输出流,缓冲流.....乱七八糟一大堆,光是这些英文单词把人背的脑袋都大了。
正因为如此,面试官就喜欢在这里考察我们的Java基础,常见的I/O流题目如下:
说一下Java中的I/O流有哪些?
你常用哪些I/O流?
输入流、输出流的区别?
......
2. 题目剖析
我们在开发时,用到I/O流的地方有很多,比如文件的上传下载,数据传输、存储,音视频编解码操作等,这些都是很重要的操作,所以面试官就很喜欢考察我们这一块的基础是否扎实。如果我们I/O流的基础不够扎实,就很难写出健壮高效的偏底层代码。
所以接下来 壹哥 会带各位详细全面的梳理一下I/O流的内容,以后再遇到有关I/O流的面试题,直接把本博客中的内容甩到面试官脸上即可,HIA HIA。
二. I/O流
1. 概念
首先 壹哥 来解释一下 “I/O流” 这个概念,如果我们连 I/O流 是个啥都不知道,也就没必要继续往下看了。在这里 壹哥 会从两部分展开介绍I/O流,即 “I/O” 与 “流”。
1.1 I/O
I/O中间用 “/”斜杠 分割,其实代表的是两个内容,即 “I” 与 “O”,分别是 “In” 与 “Out” 的缩写。
In:输入,代表着能够接收数据的数据源对象;
Out:输出,代表着能够产出数据的数据源对象。
1.2 流
接下来咱们再来看看什么是 “流”!请先在脑海里想一下,“流”是一种什么样的形态?其实Java中各个API的命名都是很形象的,绝对都做到了见名知意。
这里 “流” 就是一个很形象的概念!当我们的代码程序需要 读入数据 的时候,可以开启一个连通 数据源 的流(输入流),这个数据源可以是文件、内存、数据库,或是网络连接。同样的,当我们的代码程序需要 输出数据 的时候,可以开启一个连通 目的地 的流(输出流),这个目的地一般是指我们的代码程序。这时候你可以想象一下,我们的数据好像就在数据源与目的地之间 “流动” 起来了一样。
其实 流(stream)这个概念,一开始源于UNIX中的管道(pipe)概念。在UNIX中,管道是一条不间断的字节流,用来实现程序或进程间的通信,或读写外围设备、外部文件等。
最后 壹哥 再给各位提取一下流的概念:
流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
2. 作用
而且从上面我对I/O流的描述中,我们也可以抽取出I/O流的核心作用,如下:
I/O流可以在数据源和目的地之间搭建一个传输通道,用于处理设备与代码程序之间的数据传输,设备是指硬盘、内存、数据库、键盘录入、网络等。
一言以蔽之,I/O流屏蔽了实际的I/O设备中处理数据的各种细节,我们不必关心其内部具体的流动过程,只需知道I/O流可以用来处理设备之间的数据传输即可。
3. 分类
如果只看上面关于I/O流的概念,感觉也并没有什么难度,但是对I/O流的学习,最难的是在于其分类实在是太多。I/O流中有着不同的划分维度,如果我们根据这些不同的标准来分类的话,可以分类如下:
- 按I/O流的流动方向分为:输入流和输出流;
- 按I/O流的数据单位不同分为:字节流和字符流;
- 按I/O流的功能不同分为:节点流和处理流。
接下来 壹哥 再分别对这3种分类进行详解一下。
3.1 输入流与输出流
从I/O流的流动方向上,我们知道I/O流其实可以分为输入流与输出流,但是不少初学者总是分不清输入流与输出流,甚至会把两者搞反。所以接下来 壹哥 就再明确一下输入流与输出流的区别,我们来看下图:
在上图中,我们以家中自来水的进水与出水来形象的比喻输入流与输出流。
自来水公司相当于是数据源,我们家中的房子就相当于是目的地。自来水公司的水进入到我们家里,这就是自来水的输入;我们家中产生的污水,要排到污水处理厂,这就是自来水的输出。
在这个自来水供水、排水的过程中,我们可以想一下,输入、输出是不是一个相对的概念呢?那么相对于哪个角色呢?没错!输入、输出都是相对于我们的房子来说的,进入到房子叫做输入,流出房子叫做输出。
壹哥 再把上图中的各角色明确一下:
数据源文件:就是上图中自来水公司的水池,用于提供自来水(数据);
输入流:从自来水公司进到房子里的管道流,携带着具体的数据到家里来;
目的地:就是上图中的房子,也就是我们项目的代码程序,或者说是内存;
输出流:从家中流出到污水厂的管道流,携带着具体的数据到污水厂;
数据目标文件:最后的污水厂,其实也就是用于持久化存储污水(数据)的地方,其实也是一种数据源。
所以,I/O流中的输入流与输出流,入与出都是相对于内存而言的。从某个数据源读取数据到内存中,被称为输入流;从内存中把数据持久化保存到其他设备上,则被称为输出流。简单一句话,流向内存是输入流,流出内存的输出流。我们再来看下图:
另外要注意,我们可以从输入流中读取信息,但不能对它写;可以对输出流进行写操作,但不能从中读。所以输入也叫做读取数据,输出也叫做作写出数据!
至此,你应该不会再把输入流和输出流搞反了吧,是不是应该给 壹哥 点个赞,hiahia......
3.2 字节流与字符流
上面 壹哥 说了,按照数据流的数据单位不同,I/O流可以分为字节流与字符流,两者的区别如下:
字节流:字节流以字节(8bit)为单位,一次读入或写出8位二进制数据;字节流能处理所有类型的数据(如图片、音频、视频等);
字符流:字符流以字符为单位,根据码表映射字符,一次可能读多个字节,一次读入或写出16位二进制数据;字符流只能处理字符类型的数据。
这是因为在Java中,一个字节的空间是1个Byte,即8位;而一个字符的空间是2个Byte,即16位。另外Java的字节是有符号类型,字符是无符号类型!
另外我们要知道,在计算机里,一切文件数据(文本、图片、视频等)在存储时,都是以二进制的形式保存的,即都是一个一个的字节,在传输时也一样如此。所以,字节流可以传输任意类型的文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
但是因为数据编码的不同,为了对字符进行高效的操作,就有了字符流。字符流的本质其实也是基于字节流,在进行读取时去查了指定的码表,而字节流直接读取数据会有乱码的问题(读中文会乱码)。
所以字节流和字符流的原理其实也是相同的,只不过处理的数据单位大小不同而已。一般在Java关于I/O流的API里,后缀是Stream的是字节流,而后缀是Reader和Writer的是字符流。
你可能会问,我们开发时,到底是该选择字节流还是字符流呢?
对于字节流和字符流,如果我们只是要处理纯文本数据,可以优先考虑字符流。 除此之外尽量使用字节流。
3.3 节点流与处理流
另外如果从I/O流的功能角度来看,I/O流可以分为节点流和处理流,两者区别如下:
节点流:直接与数据源相连,读入或写出。
但是如果我们直接使用节点流进行操作,读写并不方便,所以为了更快的读写文件,才有了处理流。
处理流:一般会与节点流一起使用,在节点流的基础上,再套接一层,套接在节点流上的就是处理流。
4. I/O流相关API
在Java中,关于I/O流的API,共有四大类,分别如下:
InputStream---字节输入流;
OutputStream---字节输出流;
Reader-----字符输入流;
Writer------字符输出流。
即Java中字节流的抽象基类有如下2个:
InputStream,OutputStream
InputStream与OutputStream类关系如下图所示:
字符流的抽象基类有如下2个:
Reader,Writer
Reader与Writer类关系如下图所示:
我们可以用下图概括展示:
由这四个基类派生出来的子类名称,都是以其父类名作为子类名的后缀,如InputStream的子类FileInputStream,Reader的子类FileReader。
4.1 InputStream字节输入流
InputStream的常用方法:
int available() 从下一次调用此输入流的方法返回可从该输入流读取(或跳过)的字节数,而不会阻塞。 void close() 关闭此输入流并释放与流相关联的任何系统资源。 void mark(int readlimit) 标记此输入流中的当前位置。 boolean markSupported() 测试此输入流是否支持 mark和 reset方法。 abstract int read() 从输入流读取数据的下一个字节。 int read(byte[] b) 从输入流中读取一些字节数,并将它们存储到缓冲器阵列 b。 int read(byte[] b, int off, int len) 从输入流读取最多 len个字节的数据到字节数组。 byte[] readAllBytes() 从输入流读取所有剩余字节。 int readNBytes(byte[] b, int off, int len) 将所请求的字节数从输入流读入给定的字节数组。 void reset() 将此流重新定位到最后在此输入流上调用 mark方法时的位置。 long skip(long n) 跳过并丢弃来自此输入流的 n字节的数据。 long transferTo(OutputStream out) 从该输入流中读取所有字节,并按读取的顺序将字节写入给定的输出流。
4.2 OutputStream字节输出流
OutputStream字节输出流的方法:
void close() 关闭此输出流并释放与此流相关联的任何系统资源。 void flush() 刷新此输出流并强制任何缓冲的输出字节被写出。 void write(byte[] b) 将 b.length字节从指定的字节数组写入此输出流。 void write(byte[] b, int off, int len) 从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。 abstract void write(int b) 将指定的字节写入此输出流。
4.3 Reader字符输入流
Reader主要方法如下:
abstract void close() 关闭流并释放与之相关联的任何系统资源。 void mark(int readAheadLimit) 标记流中的当前位置。 boolean markSupported() 告诉这个流是否支持mark()操作。 int read() 读一个字符 int read(char[] cbuf) 将字符读入数组。 abstract int read(char[] cbuf, int off, int len) 将字符读入数组的一部分。 int read(CharBuffer target) 尝试将字符读入指定的字符缓冲区。 boolean ready() 告诉这个流是否准备好被读取。 void reset() 重置流。 long skip(long n) 跳过字符
4.4 Writer字符输出流
Writer的主要方法如下:
Writer append(char c) 将指定的字符附加到此writer Writer append(CharSequence csq) 将指定的字符序列附加到此writer Writer append(CharSequence csq, int start, int end) 将指定字符序列的子序列附加到此writer abstract void close() 关闭流,先刷新 abstract void flush() 刷新流 void write(char[] cbuf) 写入一个字符数组。 abstract void write(char[] cbuf, int off, int len) 写入字符数组的一部分 void write(int c) 写一个字符 void write(String str) 写一个字符串 void write(String str, int off, int len) 写一个字符串的一部分
4.5 完整API图
以上这些java.io包中的API,我给大家绘制了下图:
从上图中,我们可以看到,有各种各样的I/O流相关的API类,我们可以简单归纳并做如下简介:
- 文件操作流:
- FileInputStream(字节输入流);
- FileOutputStream(字节输出流);
- FileReader(字符输入流);
- FileWriter(字符输出流)
- 管道操作流:
- PipedInputStream(字节输入流);
- PipedOutStream(字节输出流);
- PipedReader(字符输入流);
- PipedWriter(字符输出流)
注意:
PipedInputStream的一个实例要和PipedOutputStream的一个实例共同使用,共同完成管道的读取写入操作,主要用于线程操作。
- 字节/字符数组操作流:
- ByteArrayInputStream(字节数组输入流);
- ByteArrayOutputStream(字节数组输出流);
- CharArrayReader(字符数组输入流);
- CharArrayWriter(字符数组输出流)
- Buffered缓冲流:
- BufferedInputStream(字节缓冲区输入流);
- BufferedOutputStream(字节缓冲区输出流);
- BufferedReader(字符缓冲区输入流);
- BufferedWriter(字符缓冲区输出流)
注意:
这是带缓冲区的处理流,缓冲流的底层从具体设备上获取数据,并将数据存储到缓冲区的数组内,通过缓冲区的read()方法从缓冲区获取具体的字符数据,这样就避免了每次都和硬盘打交道,提高了数据访问效率。
- 转化流:
- InputStreamReader
- OutputStreamWriter
注意:
转换流,从字面意思可以看出它是字节流与字符流之间的桥梁,可以将字节转为字符,或者将字符转为字节。
- 数据流:
- DataInputStream
- DataOutputStream
注意:
数据流可以解决我们输出数据类型的困难,数据流可以直接输出float类型或long类型,提高了数据读写的效率。
- 打印流:
- PrintStream
- PrintWriter
注意:
一般是打印到控制台,可以进行控制打印的地方。
- 对象流:
- ObjectInputStream
- ObjectOutputStream
注意:
ObjectOutputStream可以将Java对象的原始数据类型写出到文件,实现对象的持久存储;
ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。
- 序列化流:
- SequenceInputStream
三. 参考答案
1. Java中的I/O流有哪些
回到我们的面试题上面来,Java中的I/O流有哪些,其实用下图即可回答。
2. 常用I/O有哪些
我们知道,Java中的I/O流有很多,但是开发时并不是每一个都经常用到的,所以 壹哥 用下图给各位展示了常用的I/O流,下图中凡是带有中文解释的I/O流,基本就是我们开发时最常用的。
四. 结论
最后我们把IO流再简单总结一下。
1. 哪些流可以提高读写性能?
针对读写对象的不同,
字节流可以采用带缓冲区的BufferedInputStream和BufferedOutputStream;
字符流可以采用带缓冲区的BufferedReader和BufferedWriter。
2. Java有几种类型的流?
- 按I/O流的流动方向分为:输入流和输出流;
- 按I/O流的数据单位不同分为:字节流和字符流;
- 按I/O流的功能不同分为:节点流和处理流。
3. Java中I/O流相关API
字节流:
InputStream;
OutputStream
字符流:
Reader;
Writer
Java中其他各种流都是由它们派生出来的。
4. 操作文本文件用什么I/O流?
FileReader
FileWriter
5. 操作基本数据类型和String类型用什么流?
DataInputStream
DataOutputStream
6. 哪些I/O流可以指定字符编码?
BufferedReader
BufferedWriter
BufferedInputStream
BufferedOutputStream
7. 各I/O流API作用
FileInputStream/FileOutputStream:需要逐个字节处理原始二进制流的时候使用,效率低下。
FileReader/FileWriter:需要组个字符处理的时候使用。
StringReader/StringWriter:需要处理字符串的时候,可以将字符串保存为字符数组。
PrintStream/PrintWriter:用来包装FileOutputStream 对象,方便直接将String字符串写入文件。
Scanner:用来包装System.in流,很方便地将输入的String字符串转换成需要的数据类型。
InputStreamReader/OutputStreamReader,:字节和字符的转换桥梁,在网络通信或者处理键盘输入的时候用。
BufferedReader/BufferedWriter, BufferedInputStream/BufferedOutputStream:缓冲流用来包装字节流后者字符流,提升IO性能,BufferedReader还可以方便地读取一行,简化编程。
SequenceInputStream(InputStream s1, InputStream s2):序列流,合并流对象时使用。
ObjectInputStream、ObjectOutputStream:方法用于序列化对象并将它们写入一个流,另一个方法用于读取流并反序列化对象。
ByteArrayInputStream、ByteArrayOutputStream:用于操作字节数组。
DataInputStream、DataOutputStream:操作基本数据类型和字符串。
8. 开发时到底该选择哪种流?
我们现在已经知道了这么多的 I/O流 分类,那在开发时该选择使用哪种呢?什么时候用输出流?什么时候用字节流?我们可以根据下面三步选择适合自己的流:
- 首先到底该选择输入流还是输出流,这就要根据自己实际需求的情况来决定,如果想从程序内存中输出数据到别的设备中,那么就选择输出流,反之就选输入流;
- 然后我们再考虑数据输出时,每次是要传递一个字节还是两个字节,每次传输一个字节就选字节流,如果存在中文,那肯定就要选字符流了;
- 通过前面两步,我们就可以选出一个合适的节点流了,比如字节输入流 InputStream,如果要在此基础上增强功能,那么就在处理流中再选择一个合适的即可。
至此,壹哥 就把I/O流中的核心内容带大家都复习了一下,当然还有挺多细节没有总结到位,毕竟我这里是面试题梳理总结,不是专门的I/O流教程,请各位再结合之前学习I/O流时的内容,形成自己的知识脉络。如果你有什么想法,可以在评论区留言讨论哦。