Skip to content
Java io 6 min read

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

ConstructorWhat 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 readlimit argument to mark() is ignored by ByteArrayInputStream because 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. Its close() 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

MethodDescription
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 use writeTo(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 OutputStream without touching the file system.
  • In-memory serialization — serialize objects to bytes for caching or network transfer (see Serialization).
  • Data transformation — wrap a ByteArrayOutputStream with a GZIPOutputStream or CipherOutputStream to 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).
  • 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
Last updated June 13, 2026
Was this helpful?