InputStream和OutputStream应该是文件以及网络编程的基础,不管是对文件进行读取/写入或者从网络中读取写入都需要用到流。所以整个IO模块,我们从这两个开始介绍。
从含义上来看,InputStream就是输入流,OutputStream就是输出流,注意这个输入输出是相对于当前进程(线程)来说的,也就是,当我们写入数据到文件时,相对于我们的进程,就是输出,所以用OutputStream,当从文件读取数据时,相对于我们的进程,就是输入,所以用InputStream。
还有一点很重要,就是这里的流指的是字节流,而不是文本流,原则上文件或者网络世界的一切都是字节,不管是文本、视频还是图片。
我们要想深刻的了解一个类的含义以及用法,必须看源码,InputStream和OutputStream作为输入输出流的最上层类,源码其实很简单,我们慢慢来看。
OutputStream
OutputStream是所有代表字节输出流的类的父类。当我们要实现一个特定的字节输出流时,就可以继承这个类。
类定义
public abstract class OutputStream implements Closeable, Flushable
它实现了Closeable和Flushable这两个接口,一个用来关闭流,一个用来刷新输出。
方法
write
public abstract void write(int b) throws IOException;
write方法含义是向流中写入一个字节。它是一个抽象方法,子类需要给出对应的实现。为什么这里的参数类型是int而不是byte呢?个人觉得这是设计者自己的选择。从本质上来说,写入一个字节实际上就是写入一个8位的二进制,那范围肯定是00000000到11111111,我们可以用byte、int甚至long来表示这个范围,如果是byte作为参数类型的话,因为java中的byte本身就是有符号的,范围是-128到127,那么正好可以表示00000000到11111111这个范围,所以没有什么问题。而如果用int作为参数类型,因为java中的int是四个字节32位的,那怎么写入一个字节呢?
源码中的注释作出了解释:
/**
* Writes the specified byte to this output stream. The general
* contract for <code>write</code> is that one byte is written
* to the output stream. The byte to be written is the eight
* low-order bits of the argument <code>b</code>. The 24
* high-order bits of <code>b</code> are ignored.
* <p>
32位中的低位的8位会被写入,而高位24位会被忽略。我们可以用两个示例来看一下:
public static void main(String[] args) throws IOException {
OutputStream outputStream = new FileOutputStream("/Users/cuihualong/Desktop/io.txt",false);
outputStream.write(72);
outputStream.write(Integer.MAX_VALUE);
}
我们写入了两个字节,一个用int 72表示,一个用Integer.MAX_VALUE表示,72用int表示的话,高位的24位都是0,低位的8位就是01001000,它写入的就是01001000,Integer.MAX_VALUE是0fffffff,低位8位是11111111,所以我们写入的字节就是01001000和11111111,对应的无符号数字就是72和255,通过ASCII对照表来查找对应的字符:72对应的是H,255对应的是ÿ,我们可以打开文件看一下:
正好就是这两个字符。
这样我们就理解了write写入字节的原理。
write(byte b[])
这个方法是将字节数组中的字节按照顺序写入到输出流,它调用的是另一个write方法:
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
write(byte b[],int off,int len)
public void write(byte b[], int off, int len) throws IOException {
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]);
}
}
这个方法就是将byte数组b中的字节从第off个开始写入到流,一共写入len个字节。这个方法会先判断off和len参数是否正确,然后执行写入,注意它的写入实现方式是针对每个需要写入的字节,调用write(byte b)方法。
InputStream
类定义
public abstract class InputStream implements Closeable
类的定义相当简单,InputStream是一个抽象类,实现了Closeable,因为每个流在使用完之后都需要关闭。这个类是所有代表字节流输入的类的父类。
方法
read()
public abstract int read() throws IOException;
这个方法就是最简单的读取方法,用来读取输入流的下一个字节,与OutputStream的write方法对应,也是一个抽象方法,需要子类给出自己的实现。 读取的是字节,为什么返回值不用byte来表示?这个在上面介绍OutputStream时已经给出了解释,这里不再赘述。
read(byte b[])
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
这个方法是读取字节数组b[]长度的字节,并存入数组对应的位置,它的实现调用的是下面要讲的方法。
read(byte b[],int off,int len)
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
这个方法的含义是读取len长度的字节,并一次将它们填装到字节数组b从off开始的位置。
注意它获取每一个字节的方式是调用单字节read()方法。
InputStream和OutputStream的方法大概是对应的,例如单字节的read和write方法、多字节的read和write方法。对于多字节的read和write方法,都是通过循环调用单字节的对应方法来实现的,这实际上是一个低效的实现方式,子类如果有可能的话,应该给出更有效率的方式。
注意多字节read方法的返回值,是读取的字节的数量。
我们可以看一下OutputStream的子类FileOutputStream的多字节write方法的实现:
public void write(byte b[]) throws IOException {
writeBytes(b, 0, b.length, append);
}
多字节的write方法调用的是writeBytes方法,而writeBytes方法是一个native方法:
private native void writeBytes(byte b[], int off, int len, boolean append)
throws IOException;
并没有按照OutputStream中的实现。