OutputStream
# OutputStream
OutputStream
是 Java 标准库提供的最基本的输出流,它也是一个抽象类,是所有输出流的超类。
类的定义如下
public abstract class OutputStream implements Closeable, Flushable
# write 方法
OutputStream
的一个最重要的方法就是 void write(int b)
,签名如下:
public abstract void write(int b) throws IOException;
这个方法会写入一个字节到输出流。要注意的是,虽然传入的是 int
参数,但只会写入一个字节,即只写入 int
最低 8 位表示字节的部分(相当于 b & 0xff
)。
# close()
和 flush()
和 InputStream
类似,OutputStream
也提供了 close()
方法关闭输出流,以便释放系统资源。要特别注意:OutputStream
还提供了一个 flush()
方法,它的目的是将缓冲区的内容真正输出到目的地。
为什么要有 flush()
?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个 byte[]
数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多 IO 设备来说,一次写一个字节和一次写 1000 个字节,花费的时间几乎是完全一样的,所以 OutputStream
有个 flush()
方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个 flush()
方法,因为缓冲区写满了 OutputStream
会自动调用它,并且,在调用 close()
方法关闭 OutputStream
之前,也会自动调用 flush()
方法。
但是,在某些情况下,我们必须手动调用 flush()
方法。举个栗子:
小明正在开发一款在线聊天软件,当用户输入一句话后,就通过 OutputStream
的 write()
方法写入网络流。小明测试的时候发现,发送方输入后,接收方根本收不到任何信息,怎么肥四?
原因就在于写入网络流是先写入内存缓冲区,等缓冲区满了才会一次性发送到网络。如果缓冲区大小是 4K,则发送方要敲几千个字符后,操作系统才会把缓冲区的内容发送出去,这个时候,接收方会一次性收到大量消息。
解决办法就是每输入一句话后,立刻调用 flush()
,不管当前缓冲区是否已满,强迫操作系统把缓冲区的内容立刻发送出去。
实际上,InputStream
也有缓冲区。例如,从 FileInputStream
读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用 int read()
读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致 IO 操作。当缓冲区全部读完后继续调用 read()
,则会触发操作系统的下一次读取并再次填满缓冲区。
# FileOutputStream
我们以 FileOutputStream
为例,演示如何将若干个字节写入文件流:
OutputStream output = new FileOutputStream("readme.txt");
output.write(72); // H
output.write(101); // e
output.write(108); // l
output.write(108); // l
output.write(111); // o
output.close();
2
3
4
5
6
7
运行结果:
$ javac IODemo6FileOutputStream1.java
$ java IODemo6FileOutputStream1
$ cat readme.txt
Hello
2
3
4
每次写入一个字节非常麻烦,更常见的方法是一次性写入若干字节。这时,可以用 OutputStream
提供的重载方法 void write(byte[])
来实现:
OutputStream output = new FileOutputStream("readme.txt");
output.write("HelloWorld".getBytes("UTF-8"));
output.close();
2
3
运行结果:
$ javac IODemo6FileOutputStream2.java
$ java IODemo6FileOutputStream2
$ cat readme.txt
HelloWorld
2
3
4
上述代码没有考虑到在发生异常的情况下如何正确地关闭资源。写入过程也会经常发生 IO 错误,例如,磁盘已满,无权限写入等等。我们需要用 try(resource)
来保证 OutputStream
在无论是否发生 IO 错误的时候都能够正确地关闭。
# 如果文件不存在...
使用 new Fileoutputstream(filepath)时, 如果 filepath 指定的文件不存在, 文件输出流会帮我们自动创建一个文件。但仅仅是文件而已,如果 filepath 中包含尚未创建的目录, 就会抛出文件找不到异常。 所以在使用输出流之前, 最好先确定文件是否存在,不存在则使用 File 的 mkdirs 方法, 先创建文件目录
# 阻塞
和 InputStream
一样,OutputStream
的 write()
方法也是阻塞的。
# ByteArrayOutputStream
ByteArrayOutputStream
可以在内存中模拟一个 OutputStream
:
byte[] data;
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
output.write("HelloWrold!".getBytes());
data = output.toByteArray();
}
System.out.println(new String(data, "UTF-8"));
2
3
4
5
6
由于 OutputStream 里没有 toByteArray 方法,因此这里就为了简单,没有用 OutputStream 接受变量
运行结果:
$ javac IODemo6ByteOutputStream2.java
$ java IODemo6ByteOutputStream2
HelloWrold!
2
3
ByteArrayOutputStream
实际上是把一个 byte[]
数组在内存中变成一个 OutputStream
,虽然实际应用不多,但测试的时候,可以用它来构造一个 OutputStream
。
同时操作多个 AutoCloseable
资源时,在 try(resource) { ... }
语句中可以同时写出多个资源,用 ;
隔开。例如,同时读写两个文件:
// 读取input.txt,写入output.txt:
try (InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt"))
{
input.transferTo(output); // transferTo的作用是复制文件
}
2
3
4
5
6
# 小结
Java 标准库的 java.io.OutputStream
定义了所有输出流的超类:
FileOutputStream
实现了文件流输出;ByteArrayOutputStream
在内存中模拟一个字节流输出。
某些情况下需要手动调用 OutputStream
的 flush()
方法来强制输出缓冲区。
总是使用 try(resource)
来保证 OutputStream
正确关闭。