BufferedInputStream和BufferedOu

是时候写一篇讨论java.io包中的Buffer类的文章了,这里标题中的BufferedInputStream不仅仅指BufferedInputStream,BufferedOutputStream,也包括BufferedReader、BufferedWriter。

先来看三段代码,分别是我不同时期写的copy函数:

第一段,我刚开始用java.io的时候写的copy函数

public static void copy(InputStream inputStream, OutputStream outputStream, int bufferSize)
throws IOException {
    int length = 0;
    byte[] buffer = new byte[bufferSize];

    while((length = inputStream.read(buffer, 0, bufferSize)) > -1) {
        outputStream.write(buffer, 0, length);
    }
    outputStream.flush();
}

复制代码

第二段,我使用java.io一段事件之后的copy函数

public static void copy(InputStream inputStream, OutputStream outputStream, int bufferSize)
throws IOException {
    BufferedInputStream bufferedInputStream = null;
    BufferedOutputStream bufferedOutputStream = null;

    if(inputStream instanceof BufferedInputStream) {
        bufferedInputStream = (BufferedInputStream)inputStream;
    }
    else {
        bufferedInputStream = new BufferedInputStream(inputStream);
    }

    if(outputStream instanceof BufferedOutputStream) {
        bufferedOutputStream = (BufferedOutputStream)outputStream;
    }
    else {
        bufferedOutputStream = new BufferedOutputStream(outputStream);
    }

    int length = 0;
    byte[] buffer = new byte[bufferSize];

    while((length = bufferedInputStream.read(buffer, 0, bufferSize)) > -1) {
        bufferedOutputStream.write(buffer, 0, length);
    }
    bufferedOutputStream.flush();
    outputStream.flush();
}

复制代码

第三段,我又使用java.io一段时间之后写的copy函数

public static void copy(InputStream inputStream, OutputStream outputStream, int bufferSize)
throws IOException {
    int length = 0;
    byte[] buffer = new byte[bufferSize];

    while((length = inputStream.read(buffer, 0, bufferSize)) > -1) {
        outputStream.write(buffer, 0, length);
    }
    outputStream.flush();
}

复制代码

从上面看,第一段和第三段完全一样,没有任何区别,其中第二段使用了Buffered类。上面的代码很常见,几乎使用过java.io的人都写过类似的代码。究竟那个好呢?我的看法是第一个和第三个好,第二个纯粹没事找事,初衷是希望提升性能,但是很可惜一点性能也没有提升,反倒浪费了系统资源。说的更夸张一点,我的看法是在90%的情况下你使用java.io都不需要用到Buffered类,如果你用了好处是没有的,但是坏处倒是有那么一点。为什么这么说,首先需要搞清楚Buffered类的原理。

1. 在我们不清楚一个InputStream对象的具体类型的时候,我们假设一次read函数会触发一次IO,一次IO的代价是很高的。

2. 减少IO次数将会极大提升性能。

Buffered类的原理就是减少IO。举个例子,其实不应该这么啰嗦的,Buffered的原理看名字就应该清楚的。假设工人A是工地上垒墙的,工人B负责给A搬砖,A在20层高的楼上垒墙,B负责从楼下搬砖给A,搬一次我们称之为一次IO,B一次手工能搬10块砖。很累的,很可能赶不上A垒墙的速度。A大部分时间都在等B搬砖。

为了优化工作效率,我们给工人B找了一台小车,这个小车一次能运1000块砖到20层。这个小车我们称之为buffer,对应BufferedInputStream类里面的那个byte数组。

现在当A找B要砖的时候,B会检查自己的小车里面是否有足够的砖,A可能一次要100块,也可能一次要一块。如果小车里面的砖够了,那么B直接把小车里面的砖给A,否则B去楼下运足够的砖给A。

实际的实现逻辑比这里说的情况复杂,A可能一次要2000块,对于这种情况怎么处理有两种策略,java.io包中用的是循环的策略,某些情况可能会触发多次IO。另外一种策略可以最多只触发两次IO。具体请参考Buffered类的源码。搞清楚Buffered类的原理之后,再看上面的代码就很清楚了,上面的代码已经在使用buffer了,那自然就没必要再使用Buffered类了。那什么时候用呢,只有一种情况,就是当A向B要数据的时候每次要的数量不清楚,可能是一个byte,也可能是100个byte,这个时候就可以用Buffered,当然你不嫌麻烦也可以不用。使用Buffered最常见的场景就是你需要一个byte一个byte的读数据的时候,例如你需要读某种格式文件的结构的时候,比如读class文件,通常会这么写: 

DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream("Hello.class")));
int magic = dis.readInt();
System.out.println(magic == 0xCAFEBABE);

复制代码

如果你的某个函数只是作为一个数据搬运工,那完全没有必要使用Buffered类。
我们看不该用的时候有什么不好,首先Buffered是个类,当然要比你定义一个int加一个byte数组要重。这个要看你指定的bufferSize的情况,我们的前提条件是两种写法用的bufferSize大小是一样的。

举个例子,上面第二段代码,Buffered类默认的bufferSize都是8192,如果你在copy函数里面自己定义的那个buffer的大小是81920,是8192的10倍,这个时候非但没有提升性能,反而降低了性能。
如果不用的话,你读一次,写一次,两次IO,你用了之后由于Buffered类的策略是循环,反倒IO次数变成了20次。如果你的buffer大小也是8192,那么使用和不使用Buffered没有区别。
除非你定义的buffer的大小小于默认的8192才会有性能提升,那你为什么不直接定义为8192,并且不使用Buffered呢。
而且你定义了Buffered并且自己定义了buffer,你实际上增加了好多次内存拷贝,两个buffer之间拷贝来拷贝去,就好比在A和B之间有增加了一个人,A找B要砖,B再找C要,C推着小车跑到楼下拉了1000块砖运到楼上,
再倒手放到B的小车里面,B再从自己的小车里面拿给A,实际上没有任何意义。

不知道我说清楚了没有,实际上明白了原理之后,是不需要这么啰嗦的,只是看到不少类似的滥用Buffered类的代码,包括很多大名鼎鼎的开源软件里面都有大量类似的滥用,才忍不住要说一说。
新版的JDK里面的BufferedInputStream类对内部的byte数组做了缓存,但其他几个Buffered类没有类似的机制。