Byte Streams
Byte streams are the fundamental I/O abstraction in Java for reading and writing raw binary data. InputStream and OutputStream are the abstract base classes for all byte-oriented I/O, and their concrete subclasses cover every byte-level data source and destination: files, byte arrays in memory, network sockets, pipes between threads, and process standard streams. The critical read() contract — returning an int from 0 to 255 for valid bytes and -1 for end-of-stream — is the foundation of all stream-based binary processing. Byte streams do not perform character encoding or decoding; every byte is passed through as-is, making them correct for binary formats (images, audio, archives, serialized data, protocol buffers), and incorrect for text unless the encoding is explicitly managed. This entry covers the complete InputStream and OutputStream APIs, every major concrete byte stream class and its use case, DataInputStream and DataOutputStream for structured binary I/O, the mark/reset mechanism, available() and its correct interpretation, skipping and transferTo, and ObjectInputStream and ObjectOutputStream for Java serialization.
InputStream — The Complete API and Read Contract
// ── read() return contract — the most important detail ───────────────
try (InputStream is = new FileInputStream("data.bin")) {
int byteValue;
while ((byteValue = is.read()) != -1) { // int, not byte — crucial
byte actualByte = (byte) byteValue; // safe: value is always [0, 255]
processByte(actualByte);
}
}
// WRONG — misidentifies 0xFF as end-of-stream:
// byte b;
// while ((b = (byte) is.read()) != -1) { } // 0xFF → (byte)-1 → stops too early!
// ── read(byte[]) — partial reads are LEGAL and COMMON ────────────────
byte[] buffer = new byte[1024];
// WRONG: assumes read() fills the entire buffer:
try (InputStream is = socketInputStream) {
int n = is.read(buffer);
processAll(buffer, 0, buffer.length); // WRONG: only 'n' bytes were read, not 1024
}
// CORRECT: always use the actual count returned by read():
try (InputStream is = socketInputStream) {
byte[] buf = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buf)) != -1) {
process(buf, 0, bytesRead); // process only the bytes that were actually read
}
}
// ── readNBytes() — guaranteed to fill buffer (Java 11+) ──────────────
// For fixed-size binary protocol headers: always need exactly N bytes
try (InputStream is = new FileInputStream("protocol.bin")) {
byte[] header = new byte[16];
int actuallyRead = is.readNBytes(header, 0, 16);
if (actuallyRead < 16) {
throw new EOFException("Expected 16-byte header, got " + actuallyRead);
}
int version = ((header[0] & 0xFF) << 8) | (header[1] & 0xFF);
int bodyLen = ((header[4] & 0xFF) << 24) | ((header[5] & 0xFF) << 16)
| ((header[6] & 0xFF) << 8) | (header[7] & 0xFF);
byte[] body = is.readNBytes(bodyLen); // convenience overload — returns new array
process(version, body);
}
// ── readAllBytes() — convenience for small streams only (Java 9+) ────
try (InputStream is = new FileInputStream("small-config.json")) {
byte[] allBytes = is.readAllBytes(); // safe for small files (<few MB)
String json = new String(allBytes, StandardCharsets.UTF_8);
parseJson(json);
}
// DANGEROUS for large or unknown-size streams:
// byte[] huge = largeFileInputStream.readAllBytes(); // may throw OutOfMemoryError
// ── available() — bytes immediately available, NOT total remaining ────
try (InputStream is = new FileInputStream("file.bin")) {
System.out.println(is.available()); // = remaining file bytes (FileInputStream is special)
}
try (InputStream is = socket.getInputStream()) {
System.out.println(is.available()); // = bytes in receive buffer ONLY, not total to come
// WRONG: byte[] all = new byte[is.available()]; is.read(all); — reads only current buffer
}
// ── skip() and transferTo() ───────────────────────────────────────────
try (InputStream is = new FileInputStream("data.bin")) {
long skipped = is.skip(1024); // skip first 1KB — may skip less!
if (skipped < 1024) {
// Stream ended before we could skip 1024 bytes
System.out.println("Could only skip " + skipped + " bytes");
}
}
// transferTo — copy entire stream to output (Java 9+):
try (InputStream src = new FileInputStream("source.bin");
OutputStream dest = new FileOutputStream("dest.bin")) {
long bytesCopied = src.transferTo(dest);
System.out.println("Copied " + bytesCopied + " bytes");
}
// ── mark/reset — re-reading data from a stream ────────────────────────
try (InputStream is = new BufferedInputStream(new FileInputStream("data.bin"))) {
if (is.markSupported()) {
is.mark(100); // mark current position; readlimit=100 bytes
byte[] probe = is.readNBytes(4);
// Check magic bytes to determine file type:
if (probe[0] == 0x89 && probe[1] == 'P') {
is.reset(); // go back to marked position
processPng(is); // re-read from start
} else {
is.reset();
processOther(is);
}
}
}
// FileInputStream.markSupported() = false
// BufferedInputStream.markSupported() = true
// ByteArrayInputStream.markSupported() = trueOutputStream and Key Concrete Stream Classes
// ── ByteArrayInputStream — in-memory byte reading ───────────────────
byte[] data = {72, 101, 108, 108, 111}; // "Hello" in ASCII
try (InputStream is = new ByteArrayInputStream(data)) {
byte[] read = is.readAllBytes();
System.out.println(new String(read, StandardCharsets.US_ASCII)); // Hello
}
// Test utility: create InputStream from a string
InputStream fromString(String s) {
return new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8));
}
// ── ByteArrayOutputStream — accumulate bytes then extract ─────────────
ByteArrayOutputStream baos = new ByteArrayOutputStream(256); // initial capacity hint
try (DataOutputStream dos = new DataOutputStream(baos)) {
dos.writeInt(42);
dos.writeDouble(3.14159);
dos.writeUTF("hello");
} // dos.close() flushes to baos; baos itself doesn't need closing
byte[] serialized = baos.toByteArray(); // copy of accumulated bytes
System.out.println("Serialized " + serialized.length + " bytes");
// writeTo — no extra copy:
try (FileOutputStream fos = new FileOutputStream("serialized.bin")) {
baos.writeTo(fos); // writes bytes directly from baos buffer to file
}
// Convert to String:
ByteArrayOutputStream textBaos = new ByteArrayOutputStream();
textBaos.write("Hello, World!".getBytes(StandardCharsets.UTF_8));
String s = textBaos.toString(StandardCharsets.UTF_8); // Java 10+
System.out.println(s);
// ── PipedInputStream/PipedOutputStream — inter-thread piping ─────────
PipedOutputStream pipeOut = new PipedOutputStream();
PipedInputStream pipeIn = new PipedInputStream(pipeOut, 8192); // 8KB buffer
Thread producer = new Thread(() -> {
try (DataOutputStream dos = new DataOutputStream(pipeOut)) {
for (int i = 0; i < 100; i++) {
dos.writeInt(i);
}
} catch (IOException e) { e.printStackTrace(); }
}, "pipe-producer");
Thread consumer = new Thread(() -> {
try (DataInputStream dis = new DataInputStream(pipeIn)) {
for (int i = 0; i < 100; i++) {
int value = dis.readInt();
System.out.println("Received: " + value);
}
} catch (IOException e) { e.printStackTrace(); }
}, "pipe-consumer");
producer.start();
consumer.start();
producer.join();
consumer.join();
// ── DataInputStream/DataOutputStream — structured binary I/O ─────────
// Write a binary record:
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("record.bin")))) {
dos.writeInt(1); // 4 bytes: record ID
dos.writeLong(System.currentTimeMillis()); // 8 bytes: timestamp
dos.writeDouble(98.6); // 8 bytes: temperature
dos.writeBoolean(true); // 1 byte: flag
dos.writeUTF("John Doe"); // 2-byte length prefix + UTF-8 bytes
}
// Read the same binary record:
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream("record.bin")))) {
int id = dis.readInt();
long timestamp = dis.readLong();
double temp = dis.readDouble();
boolean flag = dis.readBoolean();
String name = dis.readUTF();
System.out.printf("id=%d ts=%d temp=%.1f flag=%b name=%s%n",
id, timestamp, temp, flag, name);
}
// ── SequenceInputStream — concatenate multiple streams ────────────────
InputStream part1 = new ByteArrayInputStream("Hello ".getBytes());
InputStream part2 = new ByteArrayInputStream("World".getBytes());
InputStream part3 = new ByteArrayInputStream("!".getBytes());
try (SequenceInputStream seq = new SequenceInputStream(
new SequenceInputStream(part1, part2), part3)) {
System.out.println(new String(seq.readAllBytes())); // Hello World!
}
// With Enumeration (for many streams):
Vector<InputStream> streams = new Vector<>();
for (int i = 0; i < 10; i++) {
streams.add(new ByteArrayInputStream(("Part" + i + " ").getBytes()));
}
try (SequenceInputStream seq2 = new SequenceInputStream(streams.elements())) {
System.out.println(new String(seq2.readAllBytes()));
}ObjectInputStream, ObjectOutputStream, and Serialization
// ── Basic serialization ──────────────────────────────────────────────
import java.io.Serializable;
class Employee implements Serializable {
private static final long serialVersionUID = 1L; // ALWAYS declare this
private final int id;
private final String name;
private double salary;
private transient String cachedDisplayName; // excluded from serialization
private transient Connection dbConnection; // non-serializable — must be transient
Employee(int id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
// Called after deserialization to restore transient fields:
private Object readResolve() {
this.cachedDisplayName = name + " (" + id + ")"; // recompute transient
return this;
}
@Override public String toString() {
return "Employee{id=" + id + ", name='" + name + "', salary=" + salary + "}";
}
}
// Write:
List<Employee> employees = List.of(
new Employee(1, "Alice", 95000.0),
new Employee(2, "Bob", 87000.0)
);
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("employees.ser")))) {
oos.writeObject(employees);
System.out.println("Serialized " + employees.size() + " employees");
}
// Read:
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream("employees.ser")))) {
@SuppressWarnings("unchecked")
List<Employee> restored = (List<Employee>) ois.readObject();
restored.forEach(System.out::println);
}
// ── Reference sharing — object graph preservation ─────────────────────
Employee sharedManager = new Employee(99, "Carol", 120000.0);
Employee worker1 = new Employee(1, "Dave", 80000.0);
Employee worker2 = new Employee(2, "Eve", 82000.0);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(sharedManager); // write #1: full object
oos.writeObject(sharedManager); // write #2: reference to #1 (not a duplicate)
oos.writeObject(worker1);
oos.writeObject(worker2);
}
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(baos.toByteArray()))) {
Employee m1 = (Employee) ois.readObject();
Employee m2 = (Employee) ois.readObject(); // same object as m1
System.out.println("Same object: " + (m1 == m2)); // true — reference preserved
}
// ── reset() — prevent memory leak for long-lived streams ──────────────
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("stream.ser")))) {
for (int i = 0; i < 100_000; i++) {
oos.writeObject(new Employee(i, "Worker-" + i, 50000.0));
if (i % 1000 == 0) {
oos.reset(); // clear reference table every 1000 objects
// Without reset(): all 100,000 objects held in memory until stream closes
}
}
}
// ── Custom serialization — writeObject/readObject ─────────────────────
class SecureEmployee implements Serializable {
private static final long serialVersionUID = 2L;
private final String name;
private transient byte[] encryptedSalary; // store encrypted
private transient double salary; // not directly serialized
SecureEmployee(String name, double salary) {
this.name = name;
this.salary = salary;
this.encryptedSalary = encrypt(salary);
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // write non-transient fields (name)
oos.writeObject(encryptedSalary); // write encrypted salary bytes
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // restore name
this.encryptedSalary = (byte[]) ois.readObject(); // read encrypted bytes
this.salary = decrypt(encryptedSalary); // restore transient salary
}
private byte[] encrypt(double v) { /* ... */ }
private double decrypt(byte[] b) { /* ... */ }
}