Following on from the previous article, we continued learning about byte stream operations in Java.

Decorator buffer flow BufferedInput/OutputStream

Decorator flow is actually a file IO flow based on a design pattern called decorator pattern, and our buffer flow is just one of them. Let’s take a look.

Before this, we used FileInputStream and FileOutputStream to read and write from disk byte by byte, which was very time-consuming.

However, our buffer stream can read a specified number of bytes from disk into memory at a time in advance, and subsequent read operations will be directly read from memory, improving efficiency. Let’s take a look at the implementation of the buffered flow:

Taking BufferedInputStream as an example, let’s briefly mention its core properties:

  • private static int DEFAULT_BUFFER_SIZE = 8192;
  • protected volatile byte buf[];
  • private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE – 8;
  • protected int count;
  • protected int pos;
  • protected int markpos = -1;
  • protected int marklimit;

Buf is a byte array used to buffer reads. Its values are continuously populated as the stream reads, and subsequent reads can be directly based on this buffer array.

DEFAULT_BUFFER_SIZE specifies the default buffer size, which is the array length of buF. MAX_BUFFER_SIZE specifies the maximum size of the buffer.

Count points to one bit after the last valid byte index in the buffer array. Pos points to the next byte index location to be read.

Markpos and marklimit are used for repeat read operations.

Next, let’s look at a few sample constructors for BufferedInputStream:

public BufferedInputStream(InputStream in) {
    this(in, DEFAULT_BUFFER_SIZE);
}
Copy the code
public BufferedInputStream(InputStream in, int size) {
    super(in);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}
Copy the code

In general, the former just needs to pass in an instance of InputStream that is “decorated” and use the default size buffer. The latter can explicitly specify the size of the buffer.

In addition, super(in) saves the InputStream instance into the in property field of the parent FilterInputStream class, and all actual disk reads are emitted from the InputStream instance.

Let’s look at the most important reads and how the buffer is filled.

public synchronized int read() throws IOException {
    if (pos >= count) {
        fill();
        if (pos >= count)
            return- 1; }return getBufIfOpen()[pos++] & 0xff;
}
Copy the code

This method is probably familiar, reading the next byte from the stream and returning it, but the implementation is slightly different in detail.

Count points to the position after the valid byte index in the buffer array, and pos points to the next byte index to be read. Theoretically pos can’t be greater than count, at most equal to.

If pos is equal to count, then all the valid bytes in the buffer array have been read, and the “useless” data in the buffer needs to be discarded and a new batch of data needs to be reloaded from disk to fill the buffer.

In fact, the fill method does just that. It has a lot of code, so I won’t take you to parse it. Now that you understand what it does, it’s easy to analyze its implementation.

If pos is still equal to count after the fill method is called, then the InputStream instance has not read any data from the stream, i.e. there is no data in the file stream to read. For this, see line 246 of the fill method.

In general, if the buffer is successfully filled, our read method will take a byte directly from the buffer and return it to the caller.

public synchronized int read(byte b[], int off, int len){
    //.....
}
Copy the code

This method is also “familiar”, no longer redundant explanation, implementation is similar.

The skip method is used to skip a specified number of bytes to continue reading a file stream:

public synchronized long skip(long n){
    //.....
}
Copy the code

Note that the skip method tries to skip n bytes, but does not guarantee that n bytes will be skipped. The skip method returns the actual number of bytes skipped. If the number of available bytes in the buffer array is less than n, the actual number of bytes in the buffer array that can be skipped will eventually be skipped.

One final word about the close method:

public void close() throws IOException {
    byte[] buffer;
    while( (buffer = buf) ! = null) {if (bufUpdater.compareAndSet(this, buffer, null)) {
            InputStream input = in;
            in = null;
            if(input ! = null) input.close();return;
        }
        // Else retry in case a new buf was CASed in fill()
    }
}
Copy the code

The close method empties the decorator stream and calls its close method to free up resources, eventually emptying the buffer array as well.

BufferedInputStream provides read buffering, while BufferedOutputStream provides write buffering, meaning that writes to memory are not immediately updated to disk but are kept in the buffer until the buffer is full.

protected byte buf[];

protected int count;
Copy the code

Buf represents the internal buffer, and count represents the actual data capacity in the buffer, i.e. the number of valid bytes in BUF, rather than the buF array length.

public BufferedOutputStream(OutputStream out) {
    this(out, 8192);
}

public BufferedOutputStream(OutputStream out, int size) {
    super(out);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}
Copy the code

In the same way, you must provide an instance of an OutputStream, optionally specifying the buffer size.

public synchronized void write(int b) throws IOException {
    if (count >= buf.length) {
        flushBuffer();
    }
    buf[count++] = (byte)b;
}
Copy the code

The write method checks whether the buffer can still hold the write operation. If the write operation cannot be performed, all the data in the buffer is written to the disk file. Otherwise, the buffer is written first.

Of course, BufferedOutputStream also provides the flush method, which does not have to wait until the buffer is full to write data to disk. You can also explicitly call this method to flush the buffer and update the disk file.

public synchronized void flush() throws IOException {
    flushBuffer();
    out.flush();
}
Copy the code

Buffering streams, the core of which is described above, is a significantly more efficient stream that can reduce the number of disk accesses and improve the efficiency of program execution.

The object serialization stream ObjectInput/OutputStream and the primitive decorator stream DataInput/OutputStream will not be discussed here. We’ll come back to these byte streams when we learn about serialization.


All the code, images and files in this article are stored in the cloud on my GitHub:

(https://github.com/SingleYam/overview_java)

Welcome to wechat public number: jump on the code of Gorky, all articles will be synchronized in the public number.