Byte Streams
Byte streams — InputStream and OutputStream — are Java's foundation for reading and writing raw binary data, one byte (or byte array) at a time, and are the base every other I/O stream builds on.
InputStream and OutputStream, in java.io, are the abstract base classes for reading and writing raw binary data as sequences of bytes. Unlike character streams (Reader/Writer), which interpret bytes through a text encoding, byte streams work with unencoded binary data directly — images, audio, compiled class files, network protocol payloads, or any content that isn't fundamentally text.
InputStream — Reading Bytes
InputStream is an abstract class defining the core contract for reading bytes: read() reads a single byte and returns it as an int in the range 0-255, or -1 to signal end-of-stream (an int is used, rather than a byte, specifically so -1 can be distinguished from a legitimate byte value, since a byte alone can't represent 256 possible values plus an end marker unambiguously). read(byte[] buffer) reads multiple bytes into an array at once, returning the count actually read (or -1 at end-of-stream).
Reading one byte at a time until read() returns -1, the universal end-of-stream signal across all InputStream implementations.
import java.io.InputStream;
import java.io.FileInputStream;
try (InputStream in = new FileInputStream("data.bin")) {
int b;
while ((b = in.read()) != -1) {
System.out.print(b + " ");
}
}OutputStream — Writing Bytes
OutputStream is the mirror abstract class for writing: write(int b) writes a single byte (the low 8 bits of the given int), write(byte[] buffer) writes an entire array, and write(byte[] buffer, int offset, int length) writes a subrange. flush() requests that any buffered data be pushed out immediately, and close() releases the underlying resource (and typically flushes first).
import java.io.OutputStream;
import java.io.FileOutputStream;
try (OutputStream out = new FileOutputStream("data.bin")) {
out.write(72); // writes byte value 72 ('H' in ASCII)
out.write(new byte[]{73, 33}); // writes two more bytes
}FileInputStream / FileOutputStream
FileInputStream and FileOutputStream are the concrete implementations for reading from and writing to files as raw bytes. FileOutputStream's constructor overwrites the target file by default; pass true as a second constructor argument to append instead.
A classic byte-copy loop: reading into a reusable buffer and writing only the portion actually filled (bytesRead), not the whole buffer array.
Always write buffer, 0, bytesRead rather than the whole buffer — the last read of a file typically fills fewer bytes than the buffer's capacity, and writing the full array would append leftover data from a previous read.
try (FileInputStream in = new FileInputStream("source.bin");
FileOutputStream out = new FileOutputStream("dest.bin")) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead); // write only the bytes actually read
}
}Buffering
Reading or writing one byte at a time incurs overhead per call (particularly for FileInputStream/FileOutputStream, where each call may involve a native/system-level operation). BufferedInputStream and BufferedOutputStream wrap another stream and add an internal byte buffer, batching many small reads/writes into fewer, larger underlying operations — dramatically improving performance for byte-at-a-time-style code.
Even though the loop still calls read()/write() one byte at a time, the wrapping buffered streams batch the actual underlying I/O, making this efficient.
It's still generally faster to combine buffering with array-based read(byte[])/write(byte[]) calls rather than relying on buffering alone with single-byte calls, since array-based methods reduce per-call JVM overhead as well.
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
try (var in = new BufferedInputStream(new FileInputStream("source.bin"));
var out = new BufferedOutputStream(new FileOutputStream("dest.bin"))) {
int b;
while ((b = in.read()) != -1) { // still fast, thanks to internal buffering
out.write(b);
}
} // out is flushed and closed automatically hereThe Decorator Pattern in java.io
BufferedInputStream, DataInputStream, and similar wrapper classes follow the decorator design pattern: each wraps another stream of the same abstract type, adding behavior (buffering, typed reads) while remaining substitutable wherever an InputStream/OutputStream is expected. This is why streams are often constructed as nested nested wrapper chains.
Three layers: FileInputStream does the actual file reading, BufferedInputStream adds buffering, DataInputStream adds typed binary parsing on top.
import java.io.*;
// DataInputStream adds typed reads (readInt, readDouble, etc.) on top of buffering
try (DataInputStream data = new DataInputStream(new BufferedInputStream(new FileInputStream("numbers.bin")))) {
int value = data.readInt();
double amount = data.readDouble();
System.out.println(value + ", " + amount);
}Reading and Writing Binary Data
Byte streams are the right tool whenever content is not text — images, audio/video, serialized objects, compressed archives, or any custom binary format. Attempting to process such data through character streams (Reader/Writer) risks corruption, because those apply a text encoding that assumes the bytes represent character data, which arbitrary binary content generally does not.
Files.readAllBytes is a convenient shortcut for small files; for large files, stream-based reading (as shown earlier) avoids loading everything into memory at once.
// Reading an entire small binary file into memory at once (convenience API)
byte[] allBytes = java.nio.file.Files.readAllBytes(java.nio.file.Path.of("image.png"));
System.out.println("read " + allBytes.length + " bytes");Try-With-Resources and Closing
Both InputStream and OutputStream implement Closeable, so both should always be used with try-with-resources to guarantee the underlying resource (a file handle, socket, etc.) is released even if an exception occurs mid-operation. When streams are nested (wrapped in decorators), closing the outermost wrapper closes the whole chain, since each decorator's close() delegates to the stream it wraps.
try (var out = new BufferedOutputStream(new FileOutputStream("out.bin"))) {
out.write(1);
} // closing 'out' also flushes/closes the underlying FileOutputStreamEdge Cases
read(byte[] buffer) is not guaranteed to fill the entire buffer even if more data remains in the stream — it may return fewer bytes than requested for reasons specific to the stream implementation (this is especially relevant for network streams, less so for local file streams, but code should never assume a full buffer is always filled in a single call). Always check the returned count and loop until -1 is seen when reading a complete, known-length payload.
// WRONG assumption: one read() call fills the whole buffer
byte[] buffer = new byte[1024];
in.read(buffer); // may read fewer than 1024 bytes; ignoring the return value is a bug
// CORRECT: loop until enough bytes are read or end-of-stream
int total = 0;
int n;
while (total < buffer.length && (n = in.read(buffer, total, buffer.length - total)) != -1) {
total += n;
}Practical Usage
Byte streams underlie file copying utilities, network socket I/O (Socket.getInputStream()/getOutputStream() return byte streams), reading resources bundled in a JAR (Class.getResourceAsStream()), image/media processing pipelines, and any custom binary protocol or file format implementation.
Key Takeaways
InputStream and OutputStream are the abstract base classes for byte-oriented (binary) I/O, distinct from the character-oriented Reader/Writer hierarchy.
read() returns -1 at end-of-stream, using an int return type so -1 is distinguishable from any legitimate byte value 0-255.
Always write the actually-read count (buffer, 0, bytesRead), not the full buffer array, when copying with a reusable buffer.
BufferedInputStream/BufferedOutputStream wrap another stream to batch small operations into fewer underlying I/O calls, following the decorator pattern used throughout java.io.
Use byte streams (not character streams) for genuinely binary data to avoid encoding-related corruption.
A single read(byte[]) call is not guaranteed to fill the buffer; loop until enough bytes are read or -1 is returned.
Always close streams via try-with-resources; closing an outer decorator closes the whole wrapped chain.
byte[] buffer = new byte[4096]; int bytesRead = in.read(buffer); out.write(buffer); // writes the WHOLE buffer, including stale leftover bytes byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); // writes only the bytes actually read }
Writing the entire buffer array instead of just the actually-read portion corrupts output by appending leftover bytes from a previous, larger read.
InputStream in = new FileInputStream("data.bin"); in.read(); // never closed - file handle leaked try (InputStream in = new FileInputStream("data.bin")) { in.read(); } // automatically closed, even if an exception occurs
Streams implement Closeable and hold OS-level resources (file descriptors); failing to close them, especially under exceptions, leaks resources over time.
byte[] buffer = new byte[1024]; in.read(buffer); // assumes this always fills the whole array int total = 0, n; while (total < buffer.length && (n = in.read(buffer, total, buffer.length - total)) != -1) { total += n; }
A single read(byte[]) call can return fewer bytes than the buffer's length even when more data remains; code needing a specific amount of data must loop, checking the returned count.
Why does InputStream.read() return an int rather than a byte?
A byte alone can only represent 256 distinct values (0-255), which is exactly the range of a legitimate byte read — there's no spare value left over to represent 'end of stream'. Returning an int allows -1 (outside the byte range) to unambiguously signal end-of-stream.
Why should you write(buffer, 0, bytesRead) instead of write(buffer) when copying with a reusable byte array?
read(buffer) returns the number of bytes actually placed into the array in that call, which is often less than buffer.length (especially on the final read of a file). Writing the whole array would also write stale leftover bytes from a previous, larger read, corrupting the output.
What problem does BufferedInputStream/BufferedOutputStream solve?
Reading or writing one byte (or a few bytes) at a time incurs per-call overhead, particularly for streams backed by files or sockets where each call may involve a system-level operation. Buffered streams wrap another stream and batch many small operations into fewer, larger underlying reads/writes, improving performance significantly.
What design pattern do java.io's wrapper stream classes (BufferedInputStream, DataInputStream, etc.) follow, and why does it matter?
The decorator pattern — each wrapper stream wraps another stream of the same abstract supertype, adding behavior while remaining substitutable anywhere that supertype is expected. This is why streams are commonly constructed as chains, like new DataInputStream(new BufferedInputStream(new FileInputStream(...))).
Why are byte streams preferred over character streams for genuinely binary data like images or audio?
Character streams (Reader/Writer) interpret bytes through a text encoding, which assumes the underlying bytes represent character data. Applying that assumption to non-text binary content can corrupt the data, since arbitrary binary bytes are not valid encoded text.
Is a single call to InputStream.read(byte[] buffer) guaranteed to fill the entire buffer if more data remains in the stream?
No. It may return fewer bytes than the buffer's length for implementation-specific reasons (more common for network streams than local files, but never guaranteed in general). Code that needs a specific total amount of data should loop, accumulating the returned counts, until enough bytes are read or -1 signals end-of-stream.
What happens when you close an outer wrapper stream like a BufferedOutputStream that wraps a FileOutputStream?
Closing the outer decorator closes the entire wrapped chain — each stream's close() method delegates to the stream it wraps, so closing just the outermost stream is sufficient to flush and release all underlying resources.
Write a program that copies a binary file from source to destination using a buffered byte-array copy loop.
import java.io.*; public class BinaryCopy { public static void main(String[] args) throws IOException { try (InputStream in = new BufferedInputStream(new FileInputStream("source.bin")); OutputStream out = new BufferedOutputStream(new FileOutputStream("dest.bin"))) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } } } }
The buffered streams reduce the number of underlying system-level I/O calls; the loop reads into an 8KB buffer and writes back only the actually-read portion (0, bytesRead) each iteration, continuing until read() returns -1 at end-of-file, correctly copying every byte without appending stale leftover data.
What does this program print?
ABC. The byte values 65, 66, 67 are the ASCII codes for 'A', 'B', 'C'; write(int) writes only the low 8 bits of each int argument, and reading them back as bytes then casting to char for printing reproduces the original characters.
Write a method that reads exactly n bytes from an InputStream into a byte array, looping as needed, and throws an exception if the stream ends early.
import java.io.*; public class ExactReader { static byte[] readExactly(InputStream in, int n) throws IOException { byte[] buffer = new byte[n]; int total = 0; while (total < n) { int read = in.read(buffer, total, n - total); if (read == -1) { throw new EOFException("expected " + n + " bytes, got " + total); } total += read; } return buffer; } }
The loop repeatedly calls read() into the remaining unfilled portion of the buffer, accumulating total until it reaches n; if the stream ends (-1) before n bytes have been accumulated, it throws EOFException rather than silently returning a partially-filled or short array, correctly handling the case where a single read() call fills less than requested.
OutputStream out = new FileOutputStream("out.bin");
out.write(65);
out.write(66);
out.write(67);
out.close();
InputStream in = new FileInputStream("out.bin");
int b;
while ((b = in.read()) != -1) {
System.out.print((char) b);
}