All topics
library
intermediate

Java I/O Streams: Byte vs Character

Distinguish InputStream/OutputStream (bytes) from Reader/Writer (characters) and know when to use each.

Two hierarchies. Bytes for binary, readers and writers for text.

Byte streams = a raw pipe (any liquid flows through). Character streams = a water filter (converts raw water to drinkable, accounting for impurities/encoding).

Key Concepts

1
    InputStream  / OutputStream   raw bytes
    Reader       / Writer          characters, with an encoding
2
    // binary
    try (var in = new FileInputStream("photo.jpg")) { ... }
3
    // text
    try (var r = Files.newBufferedReader(Path.of("notes.txt"))) {
      String line = r.readLine();
    }
4
Using the wrong one corrupts data. A byte stream reading text has no idea about encoding, so a multi-byte character splits across reads. A reader over a JPEG mangles it trying to decode bytes as characters.
5
Always state the encoding.
    new FileReader("f.txt")                        // platform default -- a bug
    new FileReader("f.txt", StandardCharsets.UTF_8)
    Files.readString(path)                         // UTF-8 by default
6
The platform default differs between your laptop and the server, which is how a file that reads correctly in development arrives as question marks in production. Java 18 made UTF-8 the default, which helps on new versions only.
7
Always buffer. An unbuffered read is one system call per byte:
8
    new FileInputStream(f)                          // a syscall per read
    new BufferedInputStream(new FileInputStream(f)) // 8 KB at a time
9
The difference on a large file is orders of magnitude.
10
The bridge classes convert between the two:
    new InputStreamReader(inputStream, UTF_8)
    new OutputStreamWriter(outputStream, UTF_8)
11
Which is what you need for a socket or an HTTP body, where the source is bytes and the content is text.
12
Always close, with try-with-resources. An unclosed writer may never flush, so the file ends up truncated with no error anywhere.
13
And for most jobs, skip streams entirely.
    String s = Files.readString(path);
    List<String> lines = Files.readAllLines(path);
    Files.writeString(path, s);
14
NIO.2 handles opening, buffering, decoding and closing. Reach for streams when the file is too large to hold in memory, or when you are wrapping something that is not a file.
15
What the interviewer is probing.1. "When do you use a Reader rather than an InputStream?" Probing: the binary and text split. Stalls: "They are interchangeable." Moves up: Readers decode bytes into characters using a charset; a byte stream over text splits multi-byte characters, and a Reader over a JPEG corrupts it.
16
2. "What is wrong with new FileReader(path)?" Probing: the default charset. Stalls: "Nothing." Moves up: it used the platform default charset, so a file that read correctly in development arrived as question marks on the server; always state the charset.
17
3. "Why wrap a stream in a buffered one?" Probing: syscall cost. Stalls: "It is faster." Moves up: an unbuffered read is a system call per byte; buffering reads 8KB at a time, which is orders of magnitude on a large file.
18
4. "What happens if you never close a writer?" Probing: flushing. Stalls: "The file is fine." Moves up: buffered content may never be flushed, so the file is silently truncated — which is what try-with-resources prevents.