跳至主要內容

JavaIO流设计模型

荒流2021年7月15日大约 10 分钟约 3080 字

Java 把所有设备里的有序数据抽象成流模型,简化了输入/输出处理。

1. 流的概念

Java 把不同的输入/输出源(键盘、文件、网络连接等)抽象表述为流(stream) ,通过流的方式允许 Java 程序使用相同的方式来访问不同的输入/输出源。流即是从起源(source)到接收(sink)的有序数据。

的基本概念模型把设备抽象成一个“水管”:

2. Java IO 流模型

2.1 IO 流的分类

Java 的 IO 通过 java.io 包下的类和接口来支持(有大概80个类)。IO 流可以按照多种分类方式进行划分:

Java7 在 java.nio 及其子包下提供了一系列全新的API,这些API是对原有新IO的升级,因此也被称为 NIO2,通过 NIO2 程序可以更高效地进行输入、输出操作。

2.2 IO 操作的分类

对于 IO 操作,可分成两步:

  1. 程序发出 IO 请求。由此可将 IO 操作划分为阻塞IO/非阻塞IO
    • 阻塞IO:发出IO请求会阻塞线程。
    • 非阻塞IO:发出IO请求不会阻塞线程。
  2. 完成实际的 IO 操作。由此可将IO操作划分为同步IO/异步IO
    • 同步IO:指实际的 IO 需要程序本身去执行,意味着会阻塞线程,故而是同步IO;
    • 异步IO:指实际的 IO 操作由操作系统完成,再将结果返回给应用程序,故而是异步IO。

2.3 处理流模型

Java 的处理流模型则体现了 Java 输入/输出流设计的灵活性,处理流的功能主要体现在:

45

使用处理流时,先用处理流来包装节点流,然后程序通过处理流来执行输入/输出功能,让节点流与底层的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 输入流的方法

3.2 输出流的方法

3.3 close()方法

InputStreamOutputStream都提供了close()方法关闭输出流,以便释放系统资源。但是要特别注意的是,OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地。

为什么要有flush()?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream有个flush()方法,能强制把缓冲区内容输出。

通常情况下,我们不需要调用这个flush()方法,因为缓冲区写满了OutputStream会自动调用它,并且,在调用close()方法关闭OutputStream之前,也会自动调用flush()方法。

因此,使用 Java 的 IO 流执行输出时,不要忘记执行close()方法关闭输出流,关闭输出流除可以保证流的物理资源被回收之外,可能还可以将输出流缓冲区中的数据 flush 到物理节点里。

不过,在某些情况下,我们仍然必须手动调用flush()方法。例如:

小明正在开发一款在线聊天软件,当用户输入一句话后,就通过OutputStreamwrite()方法写入网络流。小明测试的时候发现,发送方输入后,接收方根本收不到任何信息,怎么肥四?

原因就在于写入网络流是先写入内存缓冲区,等缓冲区满了才会一次性发送到网络。如果缓冲区大小是4K,则发送方要敲几千个字符后,操作系统才会把缓冲区的内容发送出去,这个时候,接收方会一次性收到大量消息。

解决办法就是每输入一句话后,立刻调用flush(),不管当前缓冲区是否已满,强迫操作系统把缓冲区的内容立刻发送出去。

实际上,InputStream也有缓冲区。例如,从FileInputStream读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read(),则会触发操作系统的下一次读取并再次填满缓冲区。

在使用处理流包装了底层节点流之后,关闭输入/输出流资源时,只要关闭最上层的处理流即可,系统会自动关闭被该处理流包装的节点流。

4. 常用类详述

通常来讲,字节流的功能比字符流的功能强大,因为计算机里所有的数据都是二进制的,而字节流可以处理所有的二进制文件。但问题是,如果使用字节流来处理文本文件,则需要使用合适的方式把这些字节转换成字符,这就增加了编程的复杂度。

一般规则:如果进行输入/输出的内容是文本内容,应该考虑使用字符流;如果进行输入/输出的内容是二进制内容,应该考虑使用字节流。

4.1 标准输入/输出流

Java使用System.inSystem.out来代表标准输入/输出,即键盘输入/显示器。

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()所需的数组时才会从原输入流中读取。

46

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