Filter streams: chaining, buffering, and data streams
◈ 5 cardsFilters are decorators wired by constructor: buffer for speed, DataOutputStream for binary protocol fields, and never PrintStream on a socket.
A filter is a decorator, and the wiring is the constructor
A filter stream wraps another stream and changes what passes through it, or adds methods for interpreting what passes through it. You connect one by handing the inner stream to the outer stream's constructor — new BufferedOutputStream(raw) — and that connection is permanent. There is no unchain, no detach. You decide the chain when you build it.
Because every filter output stream still has write, flush and close, and every filter input stream still has read, close and available, a chain is used exactly like a bare stream. That is the decorator pattern doing its job: the interface never widens, only the behaviour behind it.
Keep exactly one reference: the outermost
The single rule that prevents a whole family of bugs is this: read from or write to only the last filter in the chain. Interleaving calls to an inner stream and an outer one corrupts the outer one's buffer, because the outer stream believes it holds every byte that has moved. The defensive idiom is to overwrite the variable as you build, so there is no name left to misuse:
OutputStream out = socket.getOutputStream();
out = new BufferedOutputStream(out);
The same rule governs closing: close the outermost filter and the close cascades inward, flushing on the way. Close an inner stream directly and the outer one is left holding buffered bytes that will never leave.
Buffered streams
BufferedInputStream and BufferedOutputStream add no new public methods at all — they only override the ones they inherit. The default buffer is 2,048 bytes for input and 512 for output, and both constructors take an explicit size. For network use, pick something a little larger than a typical segment. BufferedInputStream is also one of only two java.io input streams that always support mark() and reset(), which is how you peek at the first bytes of a response and then hand the stream to whatever parser those bytes turned out to require.
Data streams for binary protocol fields
DataOutputStream and DataInputStream read and write Java's primitives in a fixed binary layout: writeInt emits four big-endian bytes, which is also network byte order, so writeInt/readInt maps directly onto the 32-bit fields real protocols define. writeUTF is the exception — it writes a two-byte length followed by a modified UTF-8 that is a Java-to-Java format, not something a C or Python peer will parse. Use it between Java programs and never as a wire format you have specified for others.
PrintStream eats your exceptions
System.out is a PrintStream, which is why it is the first filter most people meet, and it is the worst possible choice on a socket for three reasons. Its write, flush and close are declared without throws IOException — it swallows the failure and sets a private flag that only checkError() reveals, and that flag can never be cleared. println() writes a platform-dependent line separator, so the same code emits \n on Linux and \r\n on Windows while the protocol demands one specific answer. And it encodes with the platform default charset with no way to override it.
Worked example — a length-prefixed frame in one segment
Send a payload preceded by its 32-bit length, the shape of almost every binary protocol you will meet:
OutputStream raw = socket.getOutputStream();
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(raw));
out.writeInt(payload.length);
out.write(payload);
out.flush();
Read the chain outward from the socket. BufferedOutputStream collects the four header bytes and the body so they leave together rather than as two separate segments; DataOutputStream supplies writeInt and lays those four bytes down big-endian. Only out is kept — raw was never named, so it cannot be written to by accident. And flush() is still the line that sends anything at all.