在讲这两个类之前,我们先说一下流(stream),流代表着一种连续的概念,对于任何一种流,有输入必然有输出,输入和输出就代表着流的两端。就像水流,从一个地方流出必然流向另一个地方,在计算机中也如此,不过计算机中流是一种逻辑概念。比如我们拷贝文件,其实就是把源文件的字节传输到目标文件,此时源文件代表着输入流,目标文件代表着输出流。
一些牛人为了方便开发人员实现诸如文件拷贝等常见IO操作,为我们创建了java IO库。比如读取文件封装了到了输入流对象里,写入文件封装到了输出流对象里,今天要讲的InputStream和OutputStream,就分别对应着输入流和输出流。这两个类封装了操作字节流的常用方法。它们各自都拥有诸多子类,这些子类直接继承了它们的常用方法,所以一旦我们这些常用方法搞清楚,也能方便我们理解它的各种子类。
InputStream
InputStream是一个抽象类,它封装了在读取字节流时常用的一些操作,比如read,reset等。
首先我们来看一下read方法,它有三个重载
public abstract int read() throws IOException
public int read(byte b[]) throws IOException
public int read(byte b[], int off, int len) throws IOException
第一个方法表示每次读取一个字节并返回该字节
第二个方法表示每次尝试读取b.length个字节并放入b中,如果不足则返回实际读取的字节数。所以参数b可以理解为一个buffer,它不像第一个方法每次读取一个,而是读取多个,就像喝半斤牛二,你可以一口闷,也可以一次一小口。
第三个方法跟第二个一样,只是在读取时添加了更多控制,off表示从b[off]位置开始放置,len表示要放置多少字节到b中,所以如果off + len >=b.length,将会提示可爱的索引出界异常
对于后两个方法来说,读取过程其实包含两个阶段,首先从stream中依次读取,然后顺序放到buffer(参数中的字节数组b)里。
这三个方法其实要分成两类来看,第一个是一类,剩下的两个是一类,为什么这么分呢,原因在于返回值意义的不同:第一个方法的返回值代表读取的真实字节,它的范围值是-1到255,-1表示已经读到了字节流的结尾,其余的表示各个字符对应的字节码,由此可见如果我们要读取汉字时不能直接使用该方法,因为汉字的字节码超过了255。但是此方法是抽象的,各个子类可以根据情况重写。
后面两个方法的返回值表示当前读了多少个字节到字节数组b中,它不表示真实的字节码。第二个方法等价于
read(byte b[], 0, b.length)
由此可见,后两个方法的返回值一定小于等于b.length,因为一旦超过了b.length就会造成数组越界。
OutputStream
同样,对应的OutputStream也有对应的write重载方法
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
第一个方法表示写入一个字节到输出流中
第二个方法表示向输出流中写入多个字节(byte[] b)并返回实际写入的字节数,因为这个字节数组不能保证每次都被写满。举例来说,源文件里只有4个字节,而我的b的长度是1024,那么b只有前四个位置被填充,其他位置则是byte的默认值0
第三种方法与read(byte b[], int off, int len)类似,从b的[off]位置开始写入,写入的长度为len,同样,
下面我们分别用上面介绍的方法实现文件拷贝,并观察各自的差异。为了简单起见我们文件内容是一段英文文字Hello world!\r\n
使用read()和write()
try(
InputStream inputStream = new FileInputStream("d:\\temp.txt");
OutputStream outputStream = new FileOutputStream("d:\\temp1.txt")
){
int data;
while ((data = inputStream.read()) != -1){
outputStream.write(data);
}
outputStream.flush();
}
catch (IOException e){
e.printStackTrace();
}
输出
Hello world!
每次从输入流中read一个字节并write到输出流。注意输出是有回车换行两个字符的,因为转义所以显示为新行
使用read(byte[] b)和write(byte[] b)
try(
InputStream inputStream = new FileInputStream("d:\\temp.txt");
OutputStream outputStream = new FileOutputStream("d:\\temp1.txt")
){
int data;
byte[] buffer = new byte[5];
while ((data = inputStream.read(buffer)) != -1){
outputStream.write(buffer);
}
outputStream.flush();
}
catch (IOException e){
e.printStackTrace();
}
输出
Hello world!
l
注意这次多输出了一个l,为什么呢?buffer的长度是5,所以每次会尝试读取5个字符来填满buffer。因为最后一次读到buffer里的内容只有d!\r\n四个字符,而上一次读取的5个字符是worl(注意w前面还有一个空格),所以最后一次读取只覆盖了前四个字符,还剩下一个上次读取的l原封不动的放在那里。这样在输出的时候便多输出了个l,这也是在上传文件时文件莫名变大的一个原因。
如何解决呢?如果我们每次读取时都能准确的知道读取了多少个字符,然后只输出的这些字符,那么问题便迎刃而解了,是时候来介绍第三个方法了:
使用read(byte[] b)和write(byte[] b,int off,int len)
try(
InputStream inputStream = new FileInputStream("d:\\temp.txt");
OutputStream outputStream = new FileOutputStream("d:\\temp1.txt")
){
int data;
byte[] buffer = new byte[5];
while ((data = inputStream.read(buffer)) != -1){
outputStream.write(buffer,0,data);
}
outputStream.flush();
}
catch (IOException e){
e.printStackTrace();
}
输出
Hello world!
Ok, 我们已经可以看到输出了正确的内容。
练习
为了检验大家的理解,这里给出三个练习,看看结果跟你想象的是否一样
练习1
public static void main(String[] args) {
try(InputStream in = new ByteArrayInputStream("Hello world!".getBytes())){
byte[] buffer = new byte[5];
int data;
while ((data = in.read(buffer,3,2)) != -1){
for(int i=0; i < data; i++){
System.out.print((char)buffer[i]);
}
}
}
catch (IOException e){
e.printStackTrace();
}
}
练习2
public static void main(String[] args) {
try(InputStream in = new ByteArrayInputStream("Hello world!".getBytes())){
byte[] buffer = new byte[5];
int data;
while ((data = in.read(buffer,3,5)) != -1){
for(int i=0; i < data; i++){
System.out.print((char)buffer[i]);
}
}
}
catch (IOException e){
e.printStackTrace();
}
}
练习3
public static void main(String[] args) {
try(InputStream in = new ByteArrayInputStream("Hello world!".getBytes())){
byte[] buffer = new byte[6];
int data;
while ((data = in.read(buffer,3,2)) != -1){
for(int i=0; i < buffer.length; i++){
System.out.print((char)buffer[i]);
}
}
}
catch (IOException e){
e.printStackTrace();
}
}