Memra

Where bytes become text: readers, writers, encodings

◈ 5 cards

Reader and Writer are the character-side mirror of the stream hierarchy; naming the charset at the crossing point is the difference between code that works locally and code that works everywhere.

A mirror hierarchy, one conversion apart

Bytes are not characters. Java's own char is UTF-16, the network carries bytes, and something has to convert between them with a stated rule. That rule is a charset, and java.io gives you a whole parallel class hierarchy for the character side: Reader mirrors InputStream, Writer mirrors OutputStream, and the two hierarchies meet at exactly two classes. InputStreamReader wraps an InputStream and decodes its bytes into characters. OutputStreamWriter wraps an OutputStream and encodes characters into bytes. Everything else on the character side — BufferedReader, PrintWriter, FileReader — sits above that crossing.

Cross the boundary once, as early as possible on input and as late as possible on output, and name the charset when you do.

Name the charset or inherit a bug

Both crossing classes have a constructor that takes no charset, and it uses the platform default. That default is UTF-8 on most modern Linux and macOS installs and has historically been windows-1252 on a US Windows box. A program written against the default encodes one way on your machine and a different way on the marker's, and the symptom is not a crash — it is mangled accented characters in an assignment that "worked". Pass a StandardCharsets constant every time. It costs one argument and removes an entire class of failure.

ready() is not available()

InputStream.available() returns a byte count. Reader.ready() returns only a boolean. The asymmetry is forced: in a variable-width encoding like UTF-8 a character is one to four bytes, so knowing that 10 bytes are buffered tells you nothing about how many characters are there without decoding them first. A reader can promise you that a read will not block; it cannot promise you a number.

readLine() strips the terminator — you must put it back

BufferedReader.readLine() returns one line without its terminator, and it accepts a linefeed, a carriage return, or a carriage-return-linefeed pair as the end of that line. At end of stream it returns null, not -1 — a String method cannot return -1. The consequence for protocol work is direct: line-oriented protocols such as HTTP and SMTP specify \r\n, readLine() has thrown it away, so when you write your side of the exchange you write the terminator yourself. Do not use BufferedWriter.newLine(), which emits the platform separator, and do not use println(), which does the same.

Worked example — read a CRLF line protocol and answer it

An echo server that speaks a line protocol needs both crossings and the terminator discipline:

try (BufferedReader in = new BufferedReader(
         new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII));
     Writer out = new OutputStreamWriter(
         socket.getOutputStream(), StandardCharsets.US_ASCII)) {
    String request;
    while ((request = in.readLine()) != null) {
        out.write("ECHO " + request + "\r\n");
        out.flush();
    }
}

Read it as two chains over one connection. Input crosses to characters at InputStreamReader and gains readLine() at BufferedReader; output crosses back to bytes at OutputStreamWriter. null ends the loop because that is how a reader says end of stream. The literal "\r\n" is there because readLine() removed the one that arrived and the protocol still requires one. And flush() after every reply is what stops the exchange from deadlocking — the client is already blocked on its own readLine().

bytescharactersreach for it whenInputStreamReadertext is arrivingOutputStreamWritertext is going outBufferedInputStreamBufferedReaderreadLine() a protocolDataOutputStreamno twinbinary payloadPrintStreamPrintWriternever, on a socketCross once, with a named charset.
Every row is one decision. The echo server uses rows one to three; a server that also sends images uses row four for the body and must not send those bytes through row two.
NORMAL ~/memra/learn/comp-348/readers-writers-and-charsets utf-8 LF