BufferedInputStream源码阅读

前言

BufferedInputStream 使用 装饰者模式 对InputStream的功能进行了加强。通过阅读该类的代码实现,对理解装饰者模式大有助益。

继承体系

BufferedInputStream源码阅读_第1张图片

FilterInputStream

BufferedInputStream源码阅读_第2张图片
FilterInputStream 类的定义就已经用到了装饰者模式——它本身继承自抽象类InpuStream,又定义了一个InputStream的成员变量 in,而且对继承的所有抽象方法的实现都是简单的通过成员变量in来实现的,比如 close() 方法在FilterInputStream中的实现:
FilterInputStream.close().png
FilterInputStream类其实就是一个 Wrapper 类,这样做的好处是,所有想对InputStream类提供的功能进行增强的类,只需继承FilterInputStream类实现需要增强的方法即可,而无须体验直接继承InputStream类要实现其定义的所有抽象方法带来的枯燥冗余感。

BufferedInputStream

正如其名所述,它其实就是对InputStream提供了一个缓冲区。该缓冲区的存在,减少了应用程序访问磁盘的次数,从而提高了程序的性能。那它是怎么实现的呢?

字段

BufferedInputStream源码阅读_第3张图片

  1. 下面的图清楚地阐释了 count、pos和markpos是什么
    BufferedInputStream源码阅读_第4张图片
  2. buf:使用 字节数组 作为缓冲区,使用了关键字 transient 进行修饰,由此可以得出BufferedInputStream是线程安全
  3. DEFAULT_BUFFER_SIZE:默认的缓冲区大小。即当使用BufferedInputStream(InputStream) 而不是 BufferedInputStream(InputStream, int) 构造BufferedInputStream对象时,将把缓冲区大小设置为 8k
方法
  1. read() 函数
public synchronized int read() throws IOException {
    if (pos >= count) {
        // 缓冲区的数据已经使用完

        // 填充缓冲区
        fill();
        if (pos >= count) // 缓冲区依然没有内容,说明数据已经读完
            return -1;
    }
    return getBufIfOpen()[pos++] & 0xff;
}

结合代码中的注释,我们可以得出read()方法的作用是:在确保缓冲区有数据时,从缓冲区中读出一个字节。

  1. mark() 方法
public synchronized void mark(int readlimit) {
    marklimit = readlimit;
    // 将当前缓存区的起始位置保存起来
    markpos = pos;
}

该方法的作用在于记录下次 重读 的起始位置

  1. fill() 函数
private void fill() throws IOException { 
// 需要注意该方法只会在缓冲区用完后才会被调用,所以其实有些情况根本不用考虑,比如 pos < buffer.length

	// 确保字节流没有关闭
    byte[] buffer = getBufIfOpen();

    if (markpos < 0) /* 没有调用mark()方法 */
        pos = 0;            /* no mark: throw away the buffer */
    else if (pos >= buffer.length)  /* no room left in buffer */
        if (markpos > 0) {  /* can throw away early part of the buffer */
			// 调用过 mark() 方法
			
            int sz = pos - markpos;
            // 将markpos及以后的所有数据移动到缓冲区的最前方(这时pos已指向了缓冲区的最末尾处)
            System.arraycopy(buffer, markpos, buffer, 0, sz);
            pos = sz;
            markpos = 0;
        } else if (buffer.length >= marklimit) {
            markpos = -1;   /* buffer got too big, invalidate mark */
            pos = 0;        /* drop buffer contents */
        } else if (buffer.length >= MAX_BUFFER_SIZE) {
            throw new OutOfMemoryError("Required array size too large");
        } else {            /* grow buffer */
			// 这里 markpos可能为0,为了保证允许从缓冲区开头重读,所以需要对缓冲区扩容,将新读取的数据放到扩容缓冲区中
            int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
                    pos * 2 : MAX_BUFFER_SIZE;
            if (nsz > marklimit)
                nsz = marklimit;
            byte nbuf[] = new byte[nsz];
            System.arraycopy(buffer, 0, nbuf, 0, pos);
            if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
                // Can't replace buf if there was an async close.
                // Note: This would need to be changed if fill()
                // is ever made accessible to multiple threads.
                // But for now, the only way CAS can fail is via close.
                // assert buf == null;
                throw new IOException("Stream closed");
            }
            buffer = nbuf;
        }
    count = pos;
    
    // 这里就是对 read 方法进行增强的体现——通过调用底层依赖的InputStream对象的 read 方法来填充缓冲区
    // 将(因移动)而空出来的后部分缓冲区填满
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    if (n > 0)
        count = n + pos;
}

结合注释,可以看出 fill() 方法的目的在于在保证允许对缓冲区重读的前提下,尽可能地将缓冲区 buf 填满。

  1. read1() 方法
private int read1(byte[] b, int off, int len) throws IOException {
    int avail = count - pos;
    if (avail <= 0) {
        //缓冲区已经没有数据了,尝试填充缓冲区
        
        /* If the requested length is at least as large as the buffer, and
           if there is no mark/reset activity, do not bother to copy the
           bytes into the local buffer.  In this way buffered streams will
           cascade harmlessly. */
        if (len >= getBufIfOpen().length && markpos < 0) {
            return getInIfOpen().read(b, off, len);
        }
        fill();
        avail = count - pos;
        if (avail <= 0) return -1;
    }
    int cnt = (avail < len) ? avail : len;

    // 直接从缓冲区读取数据
    System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
    pos += cnt;
    return cnt;
}
  1. read(byte[], int, int) 方法
public synchronized int read(byte b[], int off, int len)
        throws IOException {
    getBufIfOpen(); // Check for closed stream

    // 通过位或运算,如果其中的一项出现零,则最终的结果为0。off + len 的出现是为了防止 off与len都为正的情况下,off+len溢出为负的情况
    if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return 0;
    }

    // 循环从缓冲区中读取数据
    int n = 0;
    for (;;) {
        int nread = read1(b, off + n, len - n);
        if (nread <= 0)
            return (n == 0) ? nread : n;
        n += nread;
        if (n >= len)
            return n;

        // if not closed but no bytes available, return
        InputStream input = in;
        if (input != null && input.available() <= 0)
            return n;
    }
}

总结

BufferedInputStream 的所有读操作都是基于底层的 字节数组缓冲区buf 进行的,其使用装饰者模式进行功能增强的代码主要体现在 fill() 方法中的 getInIfOpen().read(buffer, pos, buffer.length - pos); 。除了装饰者模式的运用外,其实还有几点值得我们学习:

  1. 使用两个字段pos、count来对线性缓冲区进行管理的思想,再加上一个额外的字段markpos还可以实现字节流的返回重读;
  2. 巧妙地使用位运算可使代码变简洁高效。

你可能感兴趣的:(Java,SE)