Memra

OutputStream, and why flush() is not optional

◈ 4 cards

The five methods every output stream has, why write(int) writes one unsigned byte, and the buffered-write deadlock that flush() exists to break.

Five methods, and that is the whole class

java.io.OutputStream is abstract, and its entire public contract is five methods: write(int b), write(byte[] data), write(byte[] data, int offset, int length), flush(), and close(). A file stream, an in-memory byte-array stream, and the stream a Socket hands you all implement exactly these. That is why Socket.getOutputStream() is declared to return the plain OutputStream supertype rather than some socket-specific class: once you know the five methods you can write to anything, and the concrete class stays hidden behind polymorphism where it belongs.

write(int b) is the fundamental one, and it is the one that catches people. It takes an int but it writes one unsigned byte. Java has no unsigned byte type, so an int stands in for the range 0 to 255. Hand it a value outside that range and only the least significant byte reaches the wire; the other three are discarded without a word and without an exception.

One byte at a time is a wire tax

Every TCP segment carries roughly 40 bytes of IP and TCP header before your payload starts. Send a kilobyte with a thousand unbuffered write(int) calls and you can push 40 KB of headers onto the network to deliver 1 KB of data. Pack the bytes into a byte[] and make one write(byte[]) call instead. On a local file the difference is a rounding error; on a network connection it is the difference between a program that works and a program that is thirty times slower than it needs to be.

Buffering changes when, not what

Chaining a BufferedOutputStream does the packing for you: it accumulates your bytes in an internal array and only writes them through to the underlying stream when the array fills, when you flush, or when you close. Its default buffer is 512 bytes. Nothing about your data changes — the same bytes arrive in the same order — but the moment they leave is now decided by the buffer, not by your call.

The hang that flush() prevents

That shift in timing is where network programs die. Suppose you write a 300-byte request into a 1,024-byte buffer and then block on a read, waiting for the reply. The buffer is not full, so nothing has been sent. The server has received nothing, so it will never reply. Your read waits forever for an answer to a question that is still sitting in your own memory. This is not an IOException you can catch and log — it is a silent deadlock with no stack trace. flush() breaks it by forcing the buffer out immediately. Flushing a stream that needs no flushing costs almost nothing, so the rule is simple: flush at every point where the other end must see your bytes before you wait on it.

Worked example — one protocol line, packed and flushed

Take a trivial text protocol in which a client sends PING 7 terminated by a carriage return and a linefeed. Build the whole line as one byte array, write it once, flush it:

static void ping(OutputStream out, int n) throws IOException {
    byte[] line = ("PING " + n + "\r\n").getBytes(StandardCharsets.US_ASCII);
    out.write(line);
    out.flush();
}

getBytes(StandardCharsets.US_ASCII) is where characters become bytes, named explicitly rather than left to whatever the machine defaults to — lesson 2.4 is entirely about why that matters. write(line) is one call, so at most one segment. out.flush() is the line that actually makes the request happen; delete it and the method still compiles, still throws nothing, and still hangs the caller.

8 bytes copiedonly on flush()when the kernel saysout.write(line)your callBufferedOutputStream512-byte buffersocket send bufferkernelthe wireTCP segmentsA buffered write isqueued, not sent.
The PING line stops twice on its way out. Only flush() moves it past the first stop, and that stop is inside your own process — which is why a missing flush looks like the network is broken when it is not.
NORMAL ~/memra/learn/comp-348/output-streams-and-flush utf-8 LF