Character Streams
Character streams, represented by the Reader and Writer abstract base classes, handle text data by abstracting away the encoding and decoding between Java's internal char/String representation (UTF-16) and the byte encoding used in files and network connections. Where byte streams treat data as raw octets, character streams treat data as Unicode characters, handling multi-byte sequences transparently according to a specified Charset. InputStreamReader and OutputStreamWriter are the bridge classes that connect byte streams to character streams, applying charset encoding on write and decoding on read. BufferedReader adds line-at-a-time reading via readLine() and multi-character buffering. PrintWriter adds print/println/printf formatting output. StringReader and StringWriter enable in-memory character stream operations on String data. This entry covers the complete Reader and Writer APIs, charset handling and the consequences of using the wrong charset, the complete class hierarchy of character streams with the use case for each, BufferedReader.readLine() semantics and the lines() stream, the bridge classes in depth, character encoding best practices, and the interaction between character streams and Java's String.lines() and Files.readString()/writeString() alternatives.
Reader and Writer Hierarchy, and the Bridge Classes
// ── InputStreamReader — byte stream to char stream bridge ───────────
// Always specify charset explicitly — never rely on platform default:
try (Reader reader = new InputStreamReader(
new FileInputStream("document.txt"), StandardCharsets.UTF_8)) {
char[] buffer = new char[4096];
int charsRead;
while ((charsRead = reader.read(buffer)) != -1) {
process(buffer, 0, charsRead); // process only charsRead chars, not buffer.length
}
}
// WRONG: relies on platform default charset — breaks across environments
try (Reader broken = new InputStreamReader(new FileInputStream("document.txt"))) {
// Platform default charset may be UTF-8 on Linux, Cp1252 on Windows — inconsistent
}
// ── OutputStreamWriter — char stream to byte stream bridge ────────────
try (Writer writer = new OutputStreamWriter(
new FileOutputStream("output.txt"), StandardCharsets.UTF_8)) {
writer.write("Hello, 世界!"); // writes UTF-8 bytes to the underlying stream
writer.write('
');
writer.write(new char[]{'A', 'B', 'C'}, 0, 3);
writer.write("substring", 3, 6); // writes chars 3..8 (inclusive of 3, exclusive of 9)
}
// ── Full stack: FileInputStream → InputStreamReader → BufferedReader ──
// This is the most explicit and portable way to read text files:
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("data.csv"), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
String[] fields = line.split(",");
processCsvRow(fields);
}
}
// Java 11+: FileReader with charset (simpler, same result):
try (BufferedReader br = new BufferedReader(
new FileReader("data.csv", StandardCharsets.UTF_8))) {
// Equivalent but cleaner
}
// ── Reader hierarchy — all concrete Reader classes ────────────────────
// StringReader: reads chars from a String
try (Reader sr = new StringReader("Hello, Reader!")) {
char[] buf = new char[5];
int n = sr.read(buf); // reads "Hello"
System.out.println(new String(buf, 0, n)); // Hello
}
// CharArrayReader: reads chars from a char[]
char[] chars = "CharArray".toCharArray();
try (Reader cr = new CharArrayReader(chars)) {
System.out.println((char) cr.read()); // C
}
// PipedReader/PipedWriter: inter-thread character piping (rarely used directly)
PipedWriter pw = new PipedWriter();
PipedReader pr = new PipedReader(pw, 4096);
new Thread(() -> {
try { pw.write("Inter-thread text"); pw.close(); }
catch (IOException e) { e.printStackTrace(); }
}).start();
try (BufferedReader br = new BufferedReader(pr)) {
System.out.println(br.readLine()); // Inter-thread text
}BufferedReader, BufferedWriter, PrintWriter — High-Level Text I/O
// ── BufferedReader.readLine() — correct and incorrect usage ──────────
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("lines.txt"), StandardCharsets.UTF_8))) {
String line;
// CORRECT: check for null to detect end-of-stream
while ((line = br.readLine()) != null) {
if (!line.isBlank()) processLine(line);
}
// WRONG: comparing to "" — empty lines return "", end-of-stream returns null
// while (!(line = br.readLine()).equals("")) { } // NullPointerException at end!
}
// ── BufferedReader.lines() — functional stream API (Java 8+) ──────────
try (BufferedReader br = Files.newBufferedReader(
Path.of("data.txt"), StandardCharsets.UTF_8)) {
long wordCount = br.lines()
.filter(line -> !line.isBlank())
.flatMap(line -> Arrays.stream(line.split("\s+")))
.filter(word -> !word.isEmpty())
.count();
System.out.println("Word count: " + wordCount);
}
// Lines stream is lazy — reads lines on demand, not all at once
try (Stream<String> lines = Files.lines(Path.of("large.txt"), StandardCharsets.UTF_8)) {
lines.filter(l -> l.contains("ERROR"))
.limit(100)
.forEach(System.out::println);
// Only reads until 100 ERROR lines found — doesn't read entire file
}
// ── BufferedWriter.newLine() vs
────────────────────────────────────
// For platform-specific line endings (e.g., Windows batch files):
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("windows.bat"), StandardCharsets.UTF_8))) {
bw.write("@echo off");
bw.newLine(); //
on Windows,
on Unix — matches platform
bw.write("echo Hello");
bw.newLine();
}
// For cross-platform files (config files, source code, version-controlled files):
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("config.txt"), StandardCharsets.UTF_8))) {
bw.write("key=value");
bw.write("
"); // ALWAYS
— never
in cross-platform files
bw.write("other=data");
bw.write("
");
}
// ── PrintWriter — convenient formatted text output to files ───────────
// From Writer (explicit charset and buffering control — recommended):
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new OutputStreamWriter(
new FileOutputStream("report.txt"), StandardCharsets.UTF_8)))) {
pw.println("Report: " + LocalDate.now());
pw.printf("Total items: %,d%n", 1_234_567);
pw.printf("Average: %.2f%n", 98.76);
pw.println("Done");
if (pw.checkError()) throw new IOException("PrintWriter write failed");
}
// From File (Java 10+: charset as second parameter):
try (PrintWriter pw = new PrintWriter(new File("output.txt"), StandardCharsets.UTF_8)) {
pw.println("Simple output");
}
// autoFlush=true: println/printf/format flush automatically:
try (PrintWriter autoFlushed = new PrintWriter(new FileWriter("live.txt"), true)) {
autoFlushed.println("Line 1"); // flushed immediately
autoFlushed.println("Line 2"); // flushed immediately
// Useful for log files that must be visible while program is running
}
// ── StringWriter — capture Writer output as String ────────────────────
StringWriter sw = new StringWriter();
try (PrintWriter pw = new PrintWriter(sw)) {
pw.printf("Name: %s%n", "Alice");
pw.printf("Score: %d%n", 95);
} // pw closed; sw still valid
String report = sw.toString();
System.out.println(report);
// Name: Alice
// Score: 95
// Testing utility: capture method output that writes to a Writer:
StringWriter capturedOutput = new StringWriter();
generateReport(new PrintWriter(capturedOutput)); // method under test
assertThat(capturedOutput.toString()).contains("Expected Section");Charset Handling, Encoding Best Practices, and Modern Alternatives
// ── Always specify charset — never rely on default ───────────────────
// WRONG: platform-dependent charset
String fromBytes = new String(bytes); // uses Charset.defaultCharset()
byte[] toBytes = string.getBytes(); // uses Charset.defaultCharset()
FileReader fr = new FileReader("file.txt"); // uses Charset.defaultCharset()
FileWriter fw = new FileWriter("file.txt"); // uses Charset.defaultCharset()
// CORRECT: always explicit
String fromBytesUTF8 = new String(bytes, StandardCharsets.UTF_8);
byte[] toBytesUTF8 = string.getBytes(StandardCharsets.UTF_8);
FileReader frExplicit = new FileReader("file.txt", StandardCharsets.UTF_8); // Java 11+
FileWriter fwExplicit = new FileWriter("file.txt", StandardCharsets.UTF_8); // Java 11+
// ── Charset detection — when encoding is unknown ──────────────────────
// If charset is truly unknown: read raw bytes and detect via BOM or external library
try (InputStream is = new FileInputStream("unknown.txt")) {
byte[] bom = is.readNBytes(3);
Charset charset;
int skip = 0;
if (bom[0] == (byte)0xEF && bom[1] == (byte)0xBB && bom[2] == (byte)0xBF) {
charset = StandardCharsets.UTF_8; skip = 3; // UTF-8 BOM
} else if (bom[0] == (byte)0xFF && bom[1] == (byte)0xFE) {
charset = StandardCharsets.UTF_16LE; skip = 2; // UTF-16 LE BOM
} else if (bom[0] == (byte)0xFE && bom[1] == (byte)0xFF) {
charset = StandardCharsets.UTF_16BE; skip = 2; // UTF-16 BE BOM
} else {
charset = StandardCharsets.UTF_8; skip = 0; // assume UTF-8 (most common)
}
// Re-read without BOM bytes consumed:
InputStream adjusted = new SequenceInputStream(
new ByteArrayInputStream(bom, skip, bom.length - skip), is);
try (BufferedReader br = new BufferedReader(
new InputStreamReader(adjusted, charset))) {
br.lines().forEach(System.out::println);
}
}
// ── CodingErrorAction — control behavior on invalid byte sequences ────
Charset utf8Strict = StandardCharsets.UTF_8;
// Default: REPLACE (replaces unmappable chars with '?')
CharsetDecoder defaultDecoder = utf8Strict.newDecoder();
// On error: replaces with U+FFFD (replacement char)
// Strict: REPORT (throws exception on invalid sequence)
CharsetDecoder strictDecoder = utf8Strict.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
// Using strict decoder with InputStreamReader:
try (Reader reader = new InputStreamReader(
new FileInputStream("strict.txt"), strictDecoder)) {
// CharacterCodingException thrown on any invalid UTF-8 sequence
String content = new BufferedReader(reader).lines()
.collect(Collectors.joining("
"));
}
// ── NIO.2 alternatives for simple file operations ────────────────────
Path path = Path.of("data.txt");
// Read entire file as String (suitable for small-to-medium files):
String content = Files.readString(path, StandardCharsets.UTF_8);
// Write String to file:
Files.writeString(path, "Hello, World!
Second line
",
StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
// Read all lines as List<String> (loads entire file into memory):
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
// Write lines (adds system line separator after each):
Files.write(path, List.of("Line 1", "Line 2", "Line 3"),
StandardCharsets.UTF_8);
// Stream lines lazily (for large files):
try (Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8)) {
stream.filter(l -> l.startsWith("ERROR"))
.forEach(System.err::println);
}
// ── Newline normalization on read ─────────────────────────────────────
// BufferedReader.readLine() strips ALL line terminators (
,
,
)
// If you need to preserve original line endings, read with char[] not readLine():
try (Reader reader = new BufferedReader(new FileReader("mixed.txt", StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
char[] buf = new char[4096];
int n;
while ((n = reader.read(buf)) != -1) {
sb.append(buf, 0, n); // preserves all original
and
characters
}
String withOriginalLineEndings = sb.toString();
}