Skip to content
Java io 5 min read

BufferedInputStream

Reading a file one byte at a time triggers a system call for every single byte — that is thousands of expensive kernel transitions for even a small file. BufferedInputStream fixes this by wrapping any InputStream and pulling data in large chunks into an internal byte array (the buffer), so your code reads from fast memory instead of the disk or network most of the time.

What BufferedInputStream Does

BufferedInputStream is a decorator — it wraps another InputStream and adds buffering on top. The class lives in java.io and extends FilterInputStream.

When you call read(), the buffer is filled in one shot (default 8 KB) from the underlying stream. Subsequent read() calls drain that in-memory buffer without touching the OS until the buffer is exhausted, at which point one more fill happens. The net effect: far fewer system calls for the same amount of data.

Tip: Always wrap file-based streams with BufferedInputStream unless you have a specific reason not to. The performance gain for sequential reads is usually 10–100×.

Basic Usage

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class BasicBufferedRead {
    public static void main(String[] args) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream("data.txt"))) {

            int byteRead;
            while ((byteRead = bis.read()) != -1) {
                System.out.print((char) byteRead);
            }
        }
    }
}

The try-with-resources block guarantees the stream is closed even if an exception is thrown. read() returns -1 at end-of-stream — a contract inherited from InputStream.

Reading into a Byte Array (Bulk Read)

Reading one byte at a time is still suboptimal even with buffering. Use read(byte[], int, int) to pull multiple bytes in a single call:

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class BulkRead {
    public static void main(String[] args) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream("image.png"))) {

            byte[] chunk = new byte[4096];
            int bytesRead;
            long total = 0;

            while ((bytesRead = bis.read(chunk, 0, chunk.length)) != -1) {
                total += bytesRead;
                // process chunk[0..bytesRead-1]
            }
            System.out.println("Total bytes read: " + total);
        }
    }
}

Output:

Total bytes read: 204800

Note: read(byte[], offset, length) returns the number of bytes actually read, which may be less than length even when more data is available. Always use the returned count, not length, when processing the array.

Custom Buffer Size

The default buffer is 8192 bytes (8 KB). You can specify a different size in the constructor:

// 64 KB buffer — beneficial for high-throughput network or disk reads
BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("large-log.bin"), 65536);

Larger buffers reduce the number of fill operations but consume more heap. For most file I/O, the default 8 KB is a sensible starting point. For network streams or large sequential files, 32–256 KB can be more efficient.

mark() and reset()

BufferedInputStream supports the mark(int readLimit) / reset() contract, which lets you re-read a section of the stream:

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;

public class MarkResetDemo {
    public static void main(String[] args) throws IOException {
        byte[] data = {10, 20, 30, 40, 50};

        try (BufferedInputStream bis = new BufferedInputStream(
                new ByteArrayInputStream(data))) {

            System.out.println(bis.read()); // 10
            bis.mark(10);                   // mark position after first byte
            System.out.println(bis.read()); // 20
            System.out.println(bis.read()); // 30
            bis.reset();                    // jump back to the mark
            System.out.println(bis.read()); // 20 again
        }
    }
}

Output:

10
20
30
20

The readLimit argument to mark() tells BufferedInputStream how many bytes you might want to re-read. If you read more than that before calling reset(), the mark may be invalidated and reset() will throw an IOException.

Warning: The underlying stream does not need to support mark()/reset(). BufferedInputStream implements the feature entirely in its own buffer. But once you read past readLimit bytes, all bets are off.

Checking markSupported()

Always verify before calling mark() on an unknown stream:

if (bis.markSupported()) {
    bis.mark(256);
    // ... read up to 256 bytes
    bis.reset();
}

BufferedInputStream.markSupported() always returns true.

Constructors at a Glance

ConstructorBuffer Size
BufferedInputStream(InputStream in)8192 bytes (default)
BufferedInputStream(InputStream in, int size)Custom size bytes

Under the Hood

Internally, BufferedInputStream holds:

  • byte[] buf — the internal buffer array
  • int count — how many valid bytes are in the buffer
  • int pos — the next byte to return from the buffer
  • int markpos — position at which mark() was called (-1 if not set)
  • int marklimit — the readLimit passed to mark()

When pos >= count (buffer exhausted), the fill() private method is invoked. It calls the underlying stream’s read(buf, 0, buf.length) to refill in one shot. This is the single system call that replaces what would have been buf.length individual calls on an unbuffered stream.

The class is not thread-safe. If two threads share a BufferedInputStream, reads and the pos/count fields can get out of sync, causing data corruption. Protect access with synchronized blocks or use a different approach (e.g., one thread owns the stream).

Since Java 9, BufferedInputStream uses sun.misc.Unsafe-backed field access for the pos and count fields to minimize volatile overhead — though this is an implementation detail that can change.

BufferedInputStream vs FileInputStream

FeatureFileInputStreamBufferedInputStream wrapping FileInputStream
Each read() callSystem call to OSServed from memory (usually)
Suitable for bulk readsYes, with byte arrayYes, even faster
mark()/reset()Not supportedSupported
OverheadMinimal per-object~8 KB heap for the buffer

For reading byte streams sequentially, the wrapped version wins almost every time.

Closing the Stream

Closing BufferedInputStream also closes the underlying InputStream. Always use try-with-resources (Java 7+) to ensure cleanup:

try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("notes.txt"))) {
    // use bis
} // bis.close() called automatically, even on exception

Tip: If you need to read text (not binary) data, consider wrapping a FileInputStream with an InputStreamReader and then a BufferedReader instead. BufferedReader adds readLine() and handles character encoding automatically.

Practical Example: Copying a File

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopy {
    public static void main(String[] args) throws IOException {
        String src  = "source.bin";
        String dest = "destination.bin";

        try (BufferedInputStream  in  = new BufferedInputStream(new FileInputStream(src));
             BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dest))) {

            byte[] buf = new byte[8192];
            int n;
            while ((n = in.read(buf)) != -1) {
                out.write(buf, 0, n);
            }
        }
        System.out.println("File copied successfully.");
    }
}

Output:

File copied successfully.

Pairing BufferedInputStream with BufferedOutputStream gives you buffering on both ends of the pipe — reads and writes both hit memory most of the time.

Last updated June 13, 2026
Was this helpful?