在Java I/O中,抽象类OutputStream是其他输出流类(如FileOutputStream)的基础类,分析一下这个类的源码很有必要。
概要
这个抽象类实现了两个接口:Closeable和Flushable。需要注意的是,在这个类的API中写到这个抽象了实现了三个接口,还包括AutoCloseable,这是因为Closeable接口继承了AutoCloseable接口的缘故。类定义如下:
public abstract class OutputStream implements Closeable, Flushable
该类使用默认的构造函数,没有参数。类中定义了1个抽象方法和4个具体方法,其中close()和flush()是对接口中方法的实现(其实是空实现,什么都没有)。方法声明分别如下:
public abstract void write(int b) throws IOException;
public void write(byte b[]) throws IOException
public void write(byte b[], int off, int len) throws IOException
以上三个方法都是用于写出字节,具体请看下文。还有两个空实现的方法:
public void flush() throws IOException {
}
public void close() throws IOException {
}
方法分析
下面是对各个方法的分析,先来看write(int b)
write(int b)
这是唯一一个抽象方法,子类中必须提供具体的实现。这个方法写出一个字节到输出流中。仔细看一下参数,发现是int类型,也就是有4个字节,那是如何处理的呢?很容易想到,只取低8位,也就意味这int的取值范围为0-255.如果提供一个超出这个范围的参数,将自动把高的24位去掉,具体地说,如果给定一个b超出范围,则将b除以256取模。例如,传递一个256,实际上写入的是1。将这个值写入输出流之后,输出流的另一端如何解析,取决于另一端。例如,对于控制台,是将其转换成ASCII码输出。子类必须实现这一方法,例如,FileOutputStream就调用本地方法来实现这个方法。如果写出过程遇到错误,抛出IOException,例如说,这个流已经被关闭,则抛出这个异常。
write(byte b[], int off, int len)
上一个方法一次只写入一个字节,许多时候是不方便的,这个方法则写入data字节数组中的len字节到输出流。参数中data就是待写入的字节(不一定全部写入),从第offset位开始写入,off+len-1是最后一位被写入的。在OutputStream这个抽象类中,直接循环调用write(int b)方法,如下:
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
API鼓励子类提供更高性能的实现方法。在这个方法中,由于字节本身就是8位,没有超出范围的情况,所以直接写入无需转换。如果传递的data为空,则抛出NullPointerException异常。如果被写入的部分超出字节数组范围,抛出IndexOutofBoundsException异常。具体地说,有以下5中情况,也就是if语句当中的5个表达式。除此之外,如果写入过程出现错误,一样抛出IOException。
write(byte b[])
这个方法其实是上一个方法的特殊情况,当off=0, len=data.length的时候,就得到了这个方法。具体实现也是通过调用上个方法来实现的,如下:
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
有一点需要说明,如果一次写入的字节数组太大,可能就会是性能下降,至于多少合适,得根据具体情况而定。比如写入网络的时候就需要小一点了,如128字节。写入文件的话,稍大一点1024.
flush()
这个方法是对Flushable接口的实现。如果在写出方法的具体实现中,用到了缓冲机制。则这个方法用于将缓冲区的数据“冲刷”到目的地去。这里需要特别理解,如果写出的方法用到了操作系统层的抽象,比如说FileOutputStream,那么该方法只能保证将缓冲区的数据提交给操作系统,但是不能保证数据被写入到磁盘文件中去。如果冲刷过程出现I/O 错误,抛出IOException。OutputStream类中的方法什么都不做。当输出流被关闭或者程序退出的时候,缓冲区的数据互自动被冲刷。对于System.out、System.err这样的输出流,当调用println()方法或者遇到换行符‘\n’的时候,缓冲数据自动被冲刷。当然,你可以在PrintStream的构造函数中传递参数来设置是否自动冲刷。
close()
这是对Closeable接口中close方法的实现。用于关闭输出流,释放相关的系统资源(如文件句柄(file handle)或者网络端口)。关闭之后,该输出流不能再被操作或者重新打开,否则抛出异常。当然,你可以不关闭,但是这不是一个好习惯,时常会出现严重问题。比如你打开文件,正在操作,那你不关闭,其他线程可能就会一直阻塞下去了。可以如下关闭输出流:
try {
OutputStream out = new FileOutputStream("numbers.dat");
// Write to the stream...
out.close( );
}
catch (IOException ex) {
System.err.println(ex);
}
当然,这样有个问题,如果在写入的时候出现异常,那么输出流的close方法就不会被执行,也就是不会被关闭。换一种方法;
OutputStream out = null;
try {
out = new FileOutputStream("numbers.dat");
// Write to the stream...
}
catch (IOException ex) {
System.err.println(ex);
}
finally {
if (out != null) {
try {
out.close( );
}
catch (IOException ex) {
System.err.println(ex);
}
}
}
注意到,这里的OutputStream声明放到了try语句外面,必须这样才能被finally中的语句访问。这样写虽然安全,但是有点丑陋就是了。如果不处理异常而是抛出,那么可以这样:
OutputStream out == null;
try {
out = new FileOutputStream("numbers.dat");
// Write to the stream...
}
finally {
if (out != null) out.close( );
}
一个自定义子类
OutputStream的子类至少要重写write(int b)方法,当然,有些子类把所有方法都重写或者重写一部分。下面这个例子写入的时候什么都不敢,只是提供一个实现参考:
完整代码如下:
import java.io.*;
public class NullOutputStream extends OutputStream {
private boolean closed = false;
public void write(int b) throws IOException {
if (closed) throw new IOException("Write to closed stream");
}
public void write(byte[] data, int offset, int length)
throws IOException {
if (data == null) throw new NullPointerException("data is null");
if (closed) throw new IOException("Write to closed stream");
}
public void close( ) {
closed = true;
}
}
这个类重写了出了flush以外的四个方法,因为写入方法什么都不干,当然没必须flush啦。实现很容易懂。不多说明。