• 从读写角度拆分

    • 读取流(InputStream、Reader的子类)
    • 写入流(OutputStream、Writer的子类)
  • 按操作数据类型可分为

    • 字节流:所有InputStream、OutputStream的子类都是字节进行操作。
    • 字符流:所有Reader、Writer的子类都是针对字符进行操作。
  • 按处理类型可分为

    • 终端流(节点流):个人理解,觉得终端流更形象贴切一些。如果某个流是直接和磁盘、内存、网络等物理终端联通来进行读写操作的,这种流就可以称为终端流或者节点流。
    • 包装流(处理流):包装流不直接和物理终端连接,所以他需要依赖终端流,因为没有终端流就没有读写目的地,包装流可以对数据读写进行过滤控制,比如类型转换、数据缓冲等。

如果我们要读取磁盘上的一个文件,那么可以使用FileInputStream,他是一个读取流、字节流、终端流。我们也可以在FileInputStream的外层包一个BufferedInputStream,他也是一个读取流,字节流,包装流,他可以实现对数据读取的缓冲。具体这个缓冲是什么意思,我们下面再具体分析。先来看一个实际的例子。

我们先通过一个文件读取的测试程序来观察一下BufferedInputStream和FileInputStream的读性能。

public class TestBuffered01 {
    public static void main(String[] args) throws Exception {
        int b = 0;
        long start = System.currentTimeMillis();
        FileInputStream fis = new FileInputStream("/home/syl/Downloads/ubuntu.iso");
        while ((b = fis.read()) != -1) { // 每次读取一个字节
            // 什么也不做,就是看看多久能读完
        System.out.println("FileInputStream read cost: " + (System.currentTimeMillis() - start));
        fis.close();
        // 重置开始时间,以下用 buffered方式读取
        start = System.currentTimeMillis();
        读取ubuntu-c1.iso文件,这个文件是ubuntu.iso的拷贝。让两个流读取同样大小但是不同的文件主要是为了比较同时从磁盘读取的性能。如果此处也读取ubuntu.iso文件的话,因为FileInputStream在之前已经读取过,一部分数据已经缓存在了内存中,后续再读取会直接从内存返回,性能比较就不公平了。
        fis = new FileInputStream("/home/syl/Downloads/ubuntu-c1.iso");
        BufferedInputStream bis = new BufferedInputStream(fis);
        while ((b = bis.read()) != -1) { // 每次读取一个字节
            // 什么也不做,就是看看多久能读完
        System.out.println("BufferedInputStream read cost: " + (System.currentTimeMillis() - start));
        bis.close();

代码很简单,分别采用FileInputStream和BufferedInputStream两个不同的类但是同样的api方法读取同样大小的不同文件(Ubuntu的安装文件,大小912M)。执行结果如下:

FileInputStream read cost: 853355

BufferedInputStream read cost: 20549

通过执行结果可见,直接采用FileInputStream的read()读取耗时将近15分钟,而采用BufferedInputStream的read()读取耗时只有20秒,性能差异巨大。接下来我们从源码角度来分析下威慑么会有这么大的差异。

通过上面的测试程序,我们可以看到无论是基于FileInputStream读取,还是基于BufferedInputStream读取,我们都是调用的read()方法来是实现字节读取的。因为他们都是InputStream的子类,所以我们先来分析下InputStream这个抽象类中的read()方法是如何定义的。

InputStream的read()方法

* Reads the next byte of data from the input stream. The value byte is * returned as an <code>int</code> in the range <code>0</code> to * <code>255</code>. If no byte is available because the end of the stream * has been reached, the value <code>-1</code> is returned. This method * blocks until input data is available, the end of the stream is detected, * or an exception is thrown. * <p> A subclass must provide an implementation of this method. * @return the next byte of data, or <code>-1</code> if the end of the * stream is reached. * @exception IOException if an I/O error occurs. public abstract int read() throws IOException;

这是个抽象方法,只有声明,没有实现,是需要子类去实现的。注释很多,所以我们接下来再来看他的子类FileInputStream对于read的实现。方法注释的也很容易理解:

从输入流中读取下一个字节的数据,这个数据以int方式返回,这个int的范围是在0-255之间(因为一个字节是8个二进制位,2的8次方能表示的无符号整数范围就是0-255),如果读不到可用数据了,就说明已经读到了流的末尾了,那么就返回-1(这也就是while循环里的判断依据)。这个方法在如下情况下会接触阻塞:

  • 输入数据可用
  • 读到了流的末尾
  • 产生了异常

换句话说,在等待数据输入(例如:System.in)或者没有读到数据的时候,会一直阻塞。

FileInputStream的read()方法

* Reads a byte of data from this input stream. This method blocks * if no input is yet available. * @return the next byte of data, or <code>-1</code> if the end of the * file is reached. * @exception IOException if an I/O error occurs. // 方法注释就是InputStream的精简版 public int read() throws IOException { return read0(); // 调用本地方法read0 // 这是个本地方法,不同的平台有不同的实现 private native int read0() throws IOException;

BufferedInputStream的部分源码

private static int DEFAULT_BUFFER_SIZE = 8192; // 默认的缓冲字节数组长度
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8; // 缓冲字节数据的最大长度
protected volatile byte buf[]; // 缓冲字节数组
protected int count; // 缓冲字节数组中已缓冲的数据长度,他应该小于等于buf.length
protected int pos; // 指的是一个读取数据的位置(应该从缓冲字节数组的哪个位置读取数据了)
* 构造方法,传入被包装的终端流(对应我们例子中的就是FileInputStream实例)
public BufferedInputStream(InputStream in) {
    this(in, DEFAULT_BUFFER_SIZE); // 调用下面的构造方法,传入默认的缓冲数组长度
* 构造方法,传入被包装的终端流以及缓冲数组的长度,最后就会按照给定长度初始化缓冲数组。
public BufferedInputStream(InputStream in, int size) {
    super(in);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    buf = new byte[size];
* 获取被包装的终端流
private InputStream getInIfOpen() throws IOException {
    InputStream input = in;
    if (input == null)
        throw new IOException("Stream closed");
    return input;
* 获取缓冲字节数组
private byte[] getBufIfOpen() throws IOException {
    byte[] buffer = buf;
    if (buffer == null)
        throw new IOException("Stream closed");
    return buffer;
* 从缓冲区读取数据。
* 首先你要知道read方法是一个一个字节的读。当缓冲区为空的时候,就先把缓冲区填满(也可能一次填不满,假设你想要读100字节,但受限于内核以及网络通信等各种配置参数,可能一次返回的不足100字节),然后一个一个的从缓冲区读。缓冲区读完了之后,需要 再接着填 或者 清空缓冲区再重新填,然后再一个一个的从缓冲区读。这其中的接着填 和 清空重新填 的动作里,pos 和 count就两个变量全程参与了。
public synchronized int read() throws IOException {
    	pos 和 count都是 BufferedInputStream对象的实例属性。初始值都是0.
        pos的作用就是标识要从缓冲的byte数组中的哪个位置读取下一个字节,每次成功读取一个,pos都要加1。
    	count的作用就是标识缓冲区有多少数据,缓冲区默认长度8192,但如果输入流只有100个字节,那么count就是100。
    	1、如果是第一次读,那么pos=0,count=0,缓冲区长度8192,假设读取回来100个字节,那么pos还是0,count=0 + 100 = 100。
    	2、这之后的100次read()调用,都从buf数组直接返回。然后pos = 100 了。
    	3、这时候 pos >= count了,因为缓冲的都读完了,所以要接着读数据然后往缓冲区填(fill),假设又读取回来500个字节,那么pos还是100,count= 100 + 500 = 600。
    	4、这之后的500次read()调用,都从buf数组直接返回。以此类推。
    	5、当count= buf.length的时候,再fill的时候,pos会从0开始,count = 0 + 读回来的字节数。
    	所以一旦pos >= count 就说明要么缓冲区还没有数据,要么缓冲区的数据都已经都读完了,总之需要重新填数据了(在后面接着填、或者清空从头填)。
    if (pos >= count) {
        fill(); // 重点是这个方法,填充数据,下面有单独解析
        if (pos >= count) //  如果执行了fill填充数据方法之后,pos还大于等于count,就说没有读取到数据,所以可以直接返回-1,表示已经读完了。
            return -1;
    // 走到这一步,就说缓冲区里有未读取的数据,那么从pos位置读一个字节返回,然后pos加1,下次从pos加1的位置接着读一个字节
    return getBufIfOpen()[pos++] & 0xff;
* 往缓冲区填充数据。
* 数据从哪来?从终端流读取而来。
private void fill() throws IOException {
    byte[] buffer = getBufIfOpen(); // 获取缓冲区字节数组(构造方法里已经初始化过),默认长度8192
    // 省略了很多代码
    count = pos;
    // 重点是这一行代码,首先会通过getInIfOpen方法获取到被自己包装的终端流,对应我们例子中的也就是FileInputStream,然后调用FileInputStream的read(byte b[], int off, int len) 方法,读取出(buffer.length - pos)长度个数据,填充到buffer数组中(也就是把数据填充到缓冲区数组buf中的pos位置后面)
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    if (n > 0)
        count = n + pos; // 如果n大于0,说明读取到了数据(但不一定就正好是buffer.length - pos个,可能不足这些个,但是没关系,总之数组能装下,然后更新数据的长度也就是count的值为pos+n)
    // 如果n小于等于0,说明没读取到数据,以为上面设置了count = pos,所有从这个方法返回到上层方法后,还有if (pos >= count)的判断,这个判断后面就会直接return -1了。

通过上面的源码分析,我们可以得出如下结论:

  • FileInputStream的read()方法调用了一个本地方法read0(),从文件一个一个字节的读取数据。

  • BufferedInputStream的read()方法是从自己的一个缓冲字节数组里一个一个的读数据。

  • 但是BufferedInputStream的缓冲数组需要通过终端流FileInputStream的read(byte b[], int off, int len) 来填充数据。

如果是这样,那我们就会发现,这BufferedInputStream的read()方法和FileInputStream的read()方法的性能差异,就变成了FileInputStream的read()方法和FileInputStream的read(byte b[], int off, int len) 方法的性能差异了。所以我们再看下FileInputStream的read(byte b[], int off, int len) 方法是如何实现的。

FileInputStream的read(byte b[], int off, int len)

* 一次性尽可能多的读取数据,当然读取数据的长度最多就会len个,读取回来的数据,从字节数组b的off位置往后写入到字节数组b中 public int read(byte b[], int off, int len) throws IOException { return readBytes(b, off, len); private native int readBytes(byte b[], int off, int len) throws IOException;

我们会发现,FileInputStream的read(byte b[], int off, int len)方法的本质就是一次性读取多个字节,减少IO次数,他最终还是调用了一个本地方法readBytes。

到这里,关于read(byte b[], int off, int len)方法有必要多说几句:

  • 该方法在抽象类InputStream中已经有定义且有默认实现。
  • FileInputStream作为InputStream的子类用他认为性能更高效的方式重新重写了该方法。

用他认为更高效的方式重写是什么意思?那就是说父类的实现很低效么?

所以我们接下来再看看InputStream的对于read(byte b[], int off, int len)方法的默认实现

InputStream的read(byte b[], int off, int len)

//  Subclasses are encouraged to provide a more efficient implementation of this method
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 {
            // 重点就看这里,这就是在一个循环里调用read()方法
            for (; i < len ; i++) {
                c = read(); // 读取一个字节
                if (c == -1) {
                    break;
                b[off + i] = (byte)c; // 把读取到的字节填充到数组里
        } catch (IOException ee) {
        return i;

所以我们可以看到InputStream的read(byte b[], int off, int len) 方法的实现是很低效的,他只是循环调用read()方法而已,还记得么?read()方法是抽象方法,是需要子类实现了,所以这个地方是一个钩子方法,父类方法逻辑中依赖子类的方法实现。所以我们的FileInputStream类一定要实现read()方法,但是可以不实现read(byte b[], int off, int len) 方法,如果FileInputStream真的没有自己重写read(byte b[], int off, int len) 方法的话,那么即便是用BufferInputStream将FileInputStream包装起来,也达不到提升性能的效果,反而还会下降一些(因为逻辑复杂了,多了一层数组的中转)。所以InputStream的read(byte b[], int off, int len) 方法的注释上说“鼓励子类用性能更好的方式重写该方法”。所以FileInputStream重写了该方法(虽然看不到本地代码是如何实现,但这不是我们要关心的重点),总是他的实现一定是批量读取一批字节,而不是一个一个读。

经过上面的分析,我们再看下面的测试用例。

public class TestBuffered02 {
    public static void main(String[] args) throws Exception {
        byte[] bytes = new byte[8192]; // 定义个接受数据的自己数组
        long start = System.currentTimeMillis();
        FileInputStream fis = new FileInputStream("/home/syl/Downloads/ubuntu-c2.iso");
        while (fis.read(bytes) != -1) { // 一次读取一批,最多能读取8192个字节
            // 什么也不做,就是看看多久能读完
        System.out.println("FileInputStream batch read bytes cost: " + (System.currentTimeMillis() - start));
        fis.close();
        // 重置开始时间,以下用 buffered方式读取
        start = System.currentTimeMillis();
        fis = new FileInputStream("/home/syl/Downloads/ubuntu-c3.iso");
        bis = new BufferedInputStream(fis);
        while (bis.read(bytes) != -1) {// 一次读取一批,最多能读取8192个字节
            // 什么也不做,就是看看多久能读完
        System.out.println("BufferedInputStream read bytes cost: " + (System.currentTimeMillis() - start));
        bis.close();        

输出结果如下:

FileInputStream batch read bytes cost: 2731
BufferedInputStream read bytes cost: 3948

该测试用例,对比的还是FileInputStream和BufferedInputStream,还是读取同样大小的文件(Ubuntu的安装文件,912M)。但是和上一次测试用力不同的是,上一次测试用例,调用的单字节的读取的方法read()。这个测试用例调用的是两个流中的批量读取字节的方法read(byte[] bytes)。

从运行时间看,二者差异不大了,而且性能都很高了。多执行几次的话(中间要穿插着读取其他文件,以便让内存中的文件缓存新陈代谢【LRU】),二者的时间会你高我低的交替,并不存在谁一直比谁快。

再继续分析源码之前,我们来思考这样一个问题:BufferedInputStream一定比FileInputStream性能高么? 或者说bufered缓冲流一定比终端流性能高么?

答案肯定是否定的,你自己就可以对比出来了,如果你用BufferedInputStream的read()方式来和FileInputStream的read(byte[])方式来对比,就会发现后者的性能更高。所以性能对比也要有背景和前提的,要基于同样的方法才有参考意义,而且就算是同样的方法也并不是用了缓冲流就一定能提高性能。接下来我们再来分析我们的测试用例: