ByteArray Streams
ByteArrayInputStream and ByteArrayOutputStream are two handy classes in java.io that let you treat an ordinary Java byte array as an I/O stream — no file, no network, no OS involvement at all. They are incredibly useful for testing, in-memory data transformation, and building data pipelines that need to interoperate with stream-based APIs.
Why ByteArray Streams Exist
Many Java APIs — serialization, image codecs, compression, cipher streams — speak in terms of InputStream and OutputStream. ByteArray streams let you feed those APIs with plain byte arrays or collect their output into a byte array without touching the file system. The result is faster (no I/O wait), side-effect-free, and easy to unit-test.
ByteArrayInputStream
ByteArrayInputStream wraps an existing byte[] and exposes it as a readable InputStream. The internal pointer moves forward as you read; you can call reset() to jump back to the start (or to a previously marked position).
Constructor Summary
| Constructor | What it does |
|---|---|
ByteArrayInputStream(byte[] buf) | Reads the entire array |
ByteArrayInputStream(byte[] buf, int offset, int length) | Reads only the specified slice |
Reading Bytes
import java.io.ByteArrayInputStream;
public class ReadExample {
public static void main(String[] args) {
byte[] data = {72, 101, 108, 108, 111}; // "Hello" in ASCII
try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
int b;
while ((b = bais.read()) != -1) {
System.out.print((char) b);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Output:
Hello
Using mark() and reset()
Because the backing store is just a byte array, markSupported() always returns true, and reset() is free — no expensive seek operation.
import java.io.ByteArrayInputStream;
import java.io.IOException;
public class MarkResetExample {
public static void main(String[] args) throws IOException {
byte[] data = "ABCDE".getBytes();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
System.out.print((char) bais.read()); // A
System.out.print((char) bais.read()); // B
bais.mark(0); // mark position after B
System.out.print((char) bais.read()); // C
System.out.print((char) bais.read()); // D
bais.reset(); // back to mark (after B)
System.out.print((char) bais.read()); // C again
}
}
}
Output:
ABCDC
Note: The
readlimitargument tomark()is ignored byByteArrayInputStreambecause the entire array is always in memory. Pass any value you like — 0 works fine.
Reading into a Buffer
You can bulk-read with read(byte[] buf, int off, int len), which is more efficient when you know how many bytes you need at once.
import java.io.ByteArrayInputStream;
import java.io.IOException;
public class BulkRead {
public static void main(String[] args) throws IOException {
byte[] source = "Hello, World!".getBytes();
try (ByteArrayInputStream bais = new ByteArrayInputStream(source)) {
byte[] chunk = new byte[5];
int bytesRead = bais.read(chunk, 0, chunk.length);
System.out.println(new String(chunk, 0, bytesRead)); // Hello
}
}
}
Output:
Hello
ByteArrayOutputStream
ByteArrayOutputStream works in the opposite direction — it collects bytes you write into a dynamically growing internal buffer. When you are done writing, call toByteArray() to get the result as a byte[], or toString() to decode it as a String.
Tip: You do not need to close a
ByteArrayOutputStream. Itsclose()method does nothing, and the buffer remains fully accessible after the stream goes out of scope. That said, using try-with-resources is still good style and costs nothing.
Writing Bytes
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class WriteExample {
public static void main(String[] args) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
baos.write("Hello".getBytes());
baos.write(',');
baos.write(" World!".getBytes());
byte[] result = baos.toByteArray();
System.out.println(new String(result));
System.out.println("Total bytes: " + baos.size());
}
}
}
Output:
Hello, World!
Total bytes: 13
Useful Methods
| Method | Description |
|---|---|
write(int b) | Writes a single byte |
write(byte[] b, int off, int len) | Writes a range from a byte array |
toByteArray() | Returns a copy of the internal buffer |
toString() | Decodes buffer using the platform default charset |
toString(String charsetName) | Decodes buffer using the given charset |
size() | Current number of bytes written |
reset() | Resets count to zero — reuse the buffer without re-allocating |
writeTo(OutputStream out) | Dumps the entire buffer into another stream |
Resetting and Reusing
reset() sets the internal write pointer back to zero without freeing the backing array. This is a cheap way to reuse an ByteArrayOutputStream in a loop:
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class ResetExample {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String[] words = {"Java", "ByteArray", "Streams"};
for (String word : words) {
baos.reset();
baos.write(word.getBytes());
System.out.println("Bytes: " + baos.size() + " | Content: " + baos);
}
}
}
Output:
Bytes: 4 | Content: Java
Bytes: 9 | Content: ByteArray
Bytes: 7 | Content: Streams
Combining Both Streams
A common pattern is to pipe data through a transformation and capture the output — here using DataOutputStream on top of ByteArrayOutputStream, then reading it back with DataInputStream on top of ByteArrayInputStream.
import java.io.*;
public class PipeExample {
public static void main(String[] args) throws IOException {
// Write structured data to memory
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (DataOutputStream dos = new DataOutputStream(baos)) {
dos.writeInt(42);
dos.writeDouble(3.14);
dos.writeUTF("Hello");
}
// Read it back from the same byte array
byte[] buffer = baos.toByteArray();
try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(buffer))) {
System.out.println(dis.readInt()); // 42
System.out.println(dis.readDouble()); // 3.14
System.out.println(dis.readUTF()); // Hello
}
}
}
Output:
42
3.14
Hello
This pattern comes up constantly when working with serialization, compression, or encryption — you need a real stream object but do not want temporary files.
Under the Hood
ByteArrayInputStream
Internally, ByteArrayInputStream stores three fields: buf (the backing array), pos (the current read position), and count (the effective length). Every read() call is simply:
// Simplified JDK source
public synchronized int read() {
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
The & 0xff widens a signed byte to an unsigned int in the range 0–255, matching the contract of InputStream.read(). No system call, no lock contention — just an array lookup and an increment. This makes it one of the fastest InputStream implementations in the JDK.
ByteArrayOutputStream
ByteArrayOutputStream maintains a byte[] buf that doubles in size whenever capacity is exhausted (similar to how ArrayList grows its backing array). The initial capacity is 32 bytes. Every write() appends to the buffer:
// Simplified — actual JDK uses ensureCapacity()
public synchronized void write(int b) {
buf[count++] = (byte) b;
}
Because the buffer doubles, amortized insertion cost is O(1). toByteArray() returns a copy of the internal array — the stream retains ownership of the original so it can be reset and reused cheaply.
Warning:
toByteArray()allocates a fresh copy each time. For large buffers in hot loops, call it once and cache the result, or usewriteTo(OutputStream)to avoid the copy entirely.
Thread Safety
Both classes synchronize their key methods (the methods are synchronized). This is the same trade-off as Vector vs ArrayList — safe for concurrent access, but potentially a bottleneck if called from many threads simultaneously. If you only use these streams from a single thread (the common case), the synchronization overhead is negligible.
Practical Use Cases
- Unit testing — capture the output of code that writes to an
OutputStreamwithout touching the file system. - In-memory serialization — serialize objects to bytes for caching or network transfer (see Serialization).
- Data transformation — wrap a
ByteArrayOutputStreamwith aGZIPOutputStreamorCipherOutputStreamto compress or encrypt in memory. - Protocol buffers / custom binary formats — build binary messages in memory and send them over a socket in one shot (see Socket Programming).
- Chaining stream decorators — feed the output of one processing stage into the input of the next without temporary files (see the DataInputStream / DataOutputStream example above).
Related Topics
- Byte vs Character Streams — understand when to use byte streams vs character streams
- Serialization — ByteArrayOutputStream is the backbone of in-memory object serialization
- DataInputStream — read Java primitives from a stream, often layered over ByteArrayInputStream
- DataOutputStream — write Java primitives to a stream, often layered over ByteArrayOutputStream
- BufferedInputStream — another decorator stream, optimized for file and network sources
- Object Streams — serialize whole object graphs, typically backed by ByteArray streams in tests