Java的InputStream类和OutputStream类

InputStream

InputStream 就是Java标准库提供的最基本的输入流。它位于 java.io 这个包里。java.io 包提供了所有 同步IO 的功能。

要特别注意的一点是,InputStream 并不是一个接口,而是一个抽象类,它是所有输入流的 超类。这个抽象类定义的一个最重要的方法就是 int read(),签名如下:

public abstract int read() throws IOException;

这个方法会读取输入流的下一个字节,并返回字节表示的 int 值(0~255)。如果已读到末尾,返回 -1 表示不能继续读取了。

FileInputStreamInputStream 的一个子类。顾名思义, FileInputStream 就是从文件流中读取数据。下面的代码演示了如何完整地读取一个 FileInputStream 的所有字节:

public void readFile() throws IOException {
    // 创建一个FileInputStream对象:
    InputStream input = new FileInputStream("src/readme.txt");
    for (;;) {
        int n = input.read(); // 反复调用read()方法,直到返回-1
        if (n == -1) {
            break;
        }
        System.out.println(n); // 打印byte的值
    }
    input.close(); // 关闭流
}

在计算机中,类似文件、网络端口这些资源,都是由操作系统统一管理的。应用程序在运行的过程中,如果打开了一个文件进行读写,完成后要及时地关闭,以便让操作系统把资源释放掉,否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其他应用程序的运行。

InputStreamOutputStream 都是通过 close() 方法来关闭流。关闭流就会释放对应的底层资源。

我们还要注意到在读取或写入 IO 流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成 IOException 异常并抛出。因此,所有与 IO 操作相关的代码都必须正确处理 IOException

仔细观察上面的代码,会发现一个潜在的问题:如果读取过程中发生了 IO 错误,InputStream 就没法正确地关闭,资源也就没法及时释放。

因此,我们需要用 try ... finally 来保证 InputStream 在无论是否发生 IO 错误的时候都能够正确地关闭:

public void readFile() throws IOException {
    InputStream input = null;
    try {
        input = new FileInputStream("readme.txt");
        int n;
        while ((n = input.read()) != -1) { // 利用while同时读取并判断
            System.out.println(n);
        }
    } finally {
        if (input != null) { input.close(); }
    }
}

try ... finally 来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的 try(resource) 的语法,只需要编写 try 语句,让编译器自动为我们关闭资源。推荐的写法如下:

public void readFile() throws IOException {
    try (InputStream input = new FileInputStream("src/readme.txt")) {
        int n;
        while ((n = input.read()) != -1) {
            System.out.println(n);
        }
    } // 编译器在此自动为我们写入finally并调用close()
}

实际上,编译器并不会特别地为 InputStream 加上自动关闭。编译器只看 try(resource = ...) 中的对象是否实现了 java.lang.AutoCloseable 接口,如果实现了,就自动加上 finally 语句并调用 close() 方法。 InputStreamOutputStream 都实现了这个接口,因此,都可以用在 try(resource) 中。

缓冲

在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream 提供了两个重载方法来支持读取多个字节:

int read(byte[] b):读取若干字节并填充到 byte[] 数组,返回读取的字节数
int read(byte[] b, int off, int len):指定 byte[] 数组的偏移量和最大填充数
利用上述方法一次读取多个字节时,需要先定义一个 byte[] 数组作为缓冲区,read() 方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read() 方法的返回值不再是字节的 int 值,而是返回实际读取了多少个字节。如果返回 -1,表示没有更多的数据了。

利用缓冲区一次读取多个字节的代码如下:

public void readFile() throws IOException {
    try (InputStream input = new FileInputStream("src/readme.txt")) {
        // 定义1000个字节大小的缓冲区:
        byte[] buffer = new byte[1000];
        int n;
        while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
            System.out.println("read " + n + " bytes.");
        }
    }
}
阻塞

在调用 InputStreamread() 方法读取数据时,我们说 read() 方法是 阻塞(Blocking) 的。它的意思是,对于下面的代码:

int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;

执行到第二行代码时,必须等 read() 方法返回后才能继续。因为读取 IO 流相比执行普通代码,速度会慢很多,因此,无法确定 read() 方法调用到底要花费多长时间。

OutputStream

InputStream 相反,OutputStream 是Java标准库提供的最基本的输出流。

InputStream 类似,OutputStream 也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是 void write(int b),签名如下:

public abstract void write(int b) throws IOException;

这个方法会写入一个字节到输出流。要注意的是,虽然传入的是 int 参数,但只会写入一个字节,即只写入int最低8位表示字节的部分。

InputStream 类似,OutputStream 也提供了 close() 方法关闭输出流,以便释放系统资源。

要特别注意:OutputStream 还提供了一个 flush() 方法,它的目的是将缓冲区的内容真正输出到目的地。

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

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

但是,在某些情况下,我们必须手动调用 flush() 方法。举个例子:

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

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

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

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

FileOutputStream

我们以 FileOutputStream 为例,演示如何将若干个字节写入文件流:

public void writeFile() throws IOException {
    OutputStream output = new FileOutputStream("out/readme.txt");
    output.write(72); // H
    output.write(101); // e
    output.write(108); // l
    output.write(108); // l
    output.write(111); // o
    output.close();
}

每次写入一个字节非常麻烦,更常见的方法是一次性写入若干字节。这时,可以用 OutputStream 提供的重载方法 void write(byte[]) 来实现:

public void writeFile() throws IOException {
    OutputStream output = new FileOutputStream("out/readme.txt");
    output.write("Hello".getBytes("UTF-8")); // Hello
    output.close();
}

InputStream 一样,上述代码没有考虑到在发生异常的情况下如何正确地关闭资源。写入过程也会经常发生 IO 错误,例如,磁盘已满,无权限写入等等。我们需要用 try(resource) 来保证 OutputStream 在无论是否发生 IO 错误的时候都能够正确地关闭:

public void writeFile() throws IOException {
    try (OutputStream output = new FileOutputStream("out/readme.txt")) {
        output.write("Hello".getBytes("UTF-8")); // Hello
    } // 编译器在此自动为我们写入finally并调用close()
}
阻塞

InputStream 一样,OutputStreamwrite() 方法也是阻塞的。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值