JavaIO流设计模型
Java 把所有设备里的有序数据抽象成流模型,简化了输入/输出处理。
1. 流的概念
Java 把不同的输入/输出源(键盘、文件、网络连接等)抽象表述为流(stream) ,通过流的方式允许 Java 程序使用相同的方式来访问不同的输入/输出源。流即是从起源(source)到接收(sink)的有序数据。
流的基本概念模型把设备抽象成一个“水管”:
输入流:输入流使用隐式的记录指针来表示当前正准备从哪个“水滴”开始读取,每当程序从输入流中取出一个或多个“水滴”后,记录指针自动向后移动。
输出流:输出流同样采样隐式的记录指针来标识当前水滴即将放入的位置,每当程序向输出流里输出一个或多个水滴后,记录指针自动向后移动。
2. Java IO 流模型
2.1 IO 流的分类
Java 的 IO 通过 java.io
包下的类和接口来支持(有大概80个类)。IO 流可以按照多种分类方式进行划分:
- 按照流向划分:
- 输入流:只能从中读取数据,不能向其写入数据。包含
InputStream
和Reader
体系。 - 输出流:只能向其写入数据,不能从中读取数据。包含
OutputStream
和Writer
体系。
- 输入流:只能从中读取数据,不能向其写入数据。包含
- 按照操作的数据单元划分:
- 字节流:以字节(8 bit)为单位。包含
InputStream
和OutputStream
体系。 - 字符流:以字符(16 bit)为单位。包含
Reader
和Writer
体系。
- 字节流:以字节(8 bit)为单位。包含
- 按照作用层级划分(使用了装饰器设计模式):
- (底层)节点流:用于和底层的物理存储节点直接关联。可以从/向一个特定的 IO 设备读/写数据的流,程序直接连接到实际的数据源。也被称作低级流。
- (上层)处理流:对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。也被称作高级流/包装流。
Java7 在 java.nio
及其子包下提供了一系列全新的API,这些API是对原有新IO的升级,因此也被称为 NIO2,通过 NIO2 程序可以更高效地进行输入、输出操作。
2.2 IO 操作的分类
对于 IO 操作,可分成两步:
- 程序发出 IO 请求。由此可将 IO 操作划分为阻塞IO/非阻塞IO:
- 阻塞IO:发出IO请求会阻塞线程。
- 非阻塞IO:发出IO请求不会阻塞线程。
- 完成实际的 IO 操作。由此可将IO操作划分为同步IO/异步IO:
- 同步IO:指实际的 IO 需要程序本身去执行,意味着会阻塞线程,故而是同步IO;
- 异步IO:指实际的 IO 操作由操作系统完成,再将结果返回给应用程序,故而是异步IO。
2.3 处理流模型
Java 的处理流模型则体现了 Java 输入/输出流设计的灵活性,处理流的功能主要体现在:

- 性能的提高:主要以增加缓冲的方式来提高输入/输出的效率;
- 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入/输出大批量的内容,而不是输入/输出一个或多个“水滴”。
使用处理流时,先用处理流来包装节点流,然后程序通过处理流来执行输入/输出功能,让节点流与底层的I/O设备、文件交互。
class PrintStreamTest {
public static void main(String[] args) {
try (
FileOutputStream fos = new FileOutputStream("test.txt");
PrintStream ps = new PrintStream(fos);
) {
ps.println("hello, world");
ps.println(new PrintStreamTest());
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
3. Java IO 流的类体系
Java 的 IO 流共涉及40多个类,它们都是从4个抽象基类派生的:
抽象基类 | 说明 |
---|---|
InputStream/Reader | 所有输入流的基类,前者是字节输入流,后者是字符输入流 |
OutputStream/Writer | 所有输出流的基类,前者是字节输出流,后者是字符输出流 |
3.1 输入流的方法
InputStream
独有方法:方法 说明 int read()
从输入流中读取单个字节,返回所读取的字节数据 int read(byte[] b)
从输入流中最多读取 b.length
个字节,并将其存储在字节数组b
中,返回实际读取的字节数int read(byte[] b, int off, int len)
从输入流中最多读取 len
个字节,并将其存储在字节数组b
中,放入数组b
中时,从off
位置开始,返回实际读取的字节数Reader
独有方法:方法 说明 int read()
从输入流中读取单个字符,返回所读取的字符数据 int read(char[] cbuf)
… int read(char[] cbuf, int off, int len)
… InputStream/Reader
公共方法:方法 说明 void mark(int readAheadLimit)
在记录指针当前位置记录一个标记 boolean markSupported()
判断此输入流是否支持 mark()
操作void reset()
将此流的记录指针重新定位到上一次记录标记(mark)的位置 long skip(long n)
记录指针向前移动 n
个字节/字符
3.2 输出流的方法
OutputStream/Writer
的相似方法:方法 说明 void write(int c)
将指定的字节/字符输出到输出流中 void write(byte[]/char[] buf)
将字节数组/字符数组中的数据输出到指定输出流中 void write(byte[]/char[] buf, int off, int len)
将字节数组/字符数组中从 off
位置开始,长度为len
的字节/字符输出到输出流中Writer
的特殊方法:因为字符流直接以字符作为操作单位,所以Writer
可以用字符串来代替字符数组。方法 说明 void write(String str)
将 str
字符串里包含的字符输出到指定输出流中void write(String str, int off, int len)
将 str
字符串里从off
位置开始,长度为len
的字符输出到指定输出流中
3.3 close()方法
InputStream
和OutputStream
都提供了close()
方法关闭输出流,以便释放系统资源。但是要特别注意的是,OutputStream
还提供了一个flush()
方法,它的目的是将缓冲区的内容真正输出到目的地。
为什么要有flush()
?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]
数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream
有个flush()
方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个flush()
方法,因为缓冲区写满了OutputStream
会自动调用它,并且,在调用close()
方法关闭OutputStream
之前,也会自动调用flush()
方法。
因此,使用 Java 的 IO 流执行输出时,不要忘记执行close()
方法关闭输出流,关闭输出流除可以保证流的物理资源被回收之外,可能还可以将输出流缓冲区中的数据 flush
到物理节点里。
不过,在某些情况下,我们仍然必须手动调用flush()
方法。例如:
小明正在开发一款在线聊天软件,当用户输入一句话后,就通过
OutputStream
的write()
方法写入网络流。小明测试的时候发现,发送方输入后,接收方根本收不到任何信息,怎么肥四?原因就在于写入网络流是先写入内存缓冲区,等缓冲区满了才会一次性发送到网络。如果缓冲区大小是4K,则发送方要敲几千个字符后,操作系统才会把缓冲区的内容发送出去,这个时候,接收方会一次性收到大量消息。
解决办法就是每输入一句话后,立刻调用
flush()
,不管当前缓冲区是否已满,强迫操作系统把缓冲区的内容立刻发送出去。
实际上,InputStream
也有缓冲区。例如,从FileInputStream
读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()
读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read()
,则会触发操作系统的下一次读取并再次填满缓冲区。
在使用处理流包装了底层节点流之后,关闭输入/输出流资源时,只要关闭最上层的处理流即可,系统会自动关闭被该处理流包装的节点流。
4. 常用类详述
通常来讲,字节流的功能比字符流的功能强大,因为计算机里所有的数据都是二进制的,而字节流可以处理所有的二进制文件。但问题是,如果使用字节流来处理文本文件,则需要使用合适的方式把这些字节转换成字符,这就增加了编程的复杂度。
一般规则:如果进行输入/输出的内容是文本内容,应该考虑使用字符流;如果进行输入/输出的内容是二进制内容,应该考虑使用字节流。
4.1 标准输入/输出流
Java使用System.in
和System.out
来代表标准输入/输出,即键盘输入/显示器。
System.in
:标准输入流,InputStream
类型,这个流是已经打开了的,默认状态对应于键盘输入;System.out
:标准输出流,PrintStream
类型,默认状态对应于屏幕输出;System.err
:标准错误信息输出流,PrintStream
类型,默认状态对应于屏幕输出。
在System
类里提供了如下三个重定向标准输入/输出的方法:
方法 | 作用 |
---|---|
static void setIn(InputStream in) | 重定向标准输入流 |
static void setOut(PrintStream out) | 重定向标准输出流 |
static void setErr(PrintStream err) | 重定向标准错误输出流 |
示例——一个方便的 API 把文本转换成基本类型或者String
:
Scanner scanner = new Scanner(System.in);
int num = scanner.nextInt();
// Scanner还有如下方法:
// nextByte()
// nextDouble()
// nextFloat()
// nextInt()
// nextInt()
// nextLong()
// nextShort()
4.2 推回输入流:PushbackInputStream/PushbackReader
两个推回输入流提供了三个方法:
方法 | 作用 |
---|---|
void unread(int b) | 将一个字节/字符推回到推回缓冲区里,从而允许重复读取刚刚读取的内容 |
void unread(byte[]/char[] buf) | 将一个字节/字符数组内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容 |
void unread(byte[]/char[] b, int off, int len) | 将一个字节/字符数组里从off 开始,长度为len 字节/字符的内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容 |
两个推回输入流都带有一个推回缓冲区,当程序调用它们的unread()
方法时,系统将会把指定数组的内容推回到该缓冲区里,而推回输入流每次调用read()
方法时总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容,且还没有装满read()所需的数组时才会从原输入流中读取。

据上,当程序创建一个PushbackInputStream
或PushbackReader
时需要指定推回缓冲区的大小,默认的推回缓冲区的长度为1。如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将引发Pushback buffer overflow
的IOException
异常。