Java Character Streams: Reader and Writer
A file menu.txt is saved in UTF-8 and contains one word: «café». A Java 17 program running on Windows with a US or Western European locale reads it with new FileReader("menu.txt") and prints café. The code has no bug: FileReader without an explicit charset used the system default, windows-1252. How to avoid this, and which character stream class to use for what, is covered below.
Character streams in Java are classes of the java.io package that read and write text as char values (16-bit UTF-16 units) rather than as raw bytes. Every character input stream extends the abstract class Reader, and every character output stream extends Writer. A file or a network connection always holds bytes, so a character stream attached to a file converts bytes to characters and back using a charset (Charset). The general split into byte and character streams is explained in the lesson Java I/O Streams.
1. The Reader class
Reader is the abstract class that every character input stream in Java extends. Its read() methods return the next character as an int from 0 to 65535, or -1 when the stream is over. On an I/O error the methods of the class throw IOException.
Reader methods
| Method | What it does | Edge case |
|---|---|---|
int read() | Reads one character and returns its code as an int from 0 to 65535 | Returns -1 at the end of the stream |
int read(char[] buffer) | Reads up to buffer.length characters into the array and returns the number actually read | May read fewer characters than the array size; returns -1 at the end of the stream |
int read(char[] buffer, int offset, int length) | Reads up to length characters into the array, starting at buffer[offset] | IndexOutOfBoundsException if offset/length go outside the array |
long skip(long n) | Skips up to n characters | Returns the number of characters actually skipped |
boolean ready() | Tells whether a character can be read without blocking | false does not mean the end of the stream — only -1 does |
long transferTo(Writer out) (Java 10) | Copies all remaining characters to a Writer | Returns the number of characters copied |
void close() | Closes the stream and releases operating system resources | Later calls to read() throw IOException: Stream closed |
Since Java 11 there is a factory method Reader.nullReader(): an empty reader that returns -1 right away. It is a handy stub in tests.
2. The Writer class
Writer is the abstract class that every character output stream extends. It defines write() and append() for writing characters, arrays and strings, flush() for pushing buffered data out, and close() for closing the destination. On an I/O error the methods throw IOException.
Writer methods
| Method | What it does | Edge case |
|---|---|---|
void write(int c) | Writes one character | Only the low 16 bits of the number are written |
void write(char[] buffer) | Writes the whole character array | NullPointerException if the array is null |
void write(char[] buffer, int offset, int length) | Writes length characters of the array, starting at offset | IndexOutOfBoundsException if the range goes outside the array |
void write(String str) | Writes a string | NullPointerException for null |
void write(String str, int offset, int length) | Writes a substring of length characters, starting at offset | IndexOutOfBoundsException if the range goes outside the string |
Writer append(CharSequence csq) | Same as write(), but returns the same Writer, so calls can be chained: w.append("a").append('b') | For null it writes the string "null" instead of throwing |
void flush() | Pushes characters collected in buffers to the destination | Not only for BufferedWriter: even FileWriter keeps bytes in the internal buffer of its encoder, and without flush()/close() they never reach the file |
void close() | Flushes the buffer (flush()) and closes the stream | Later calls to write() throw IOException |
Like Reader, Java 11 added Writer.nullWriter(): a writer that silently discards everything written to it.
3. FileReader and FileWriter
FileReader reads characters from a text file, and FileWriter writes characters to a file. Their constructors take a path as a string or a File object (see the lesson Java File Class). Under the hood they are subclasses of InputStreamReader and OutputStreamWriter: they open a file byte stream and convert bytes to characters using a charset.
FileReader and FileWriter constructors
| Constructor | What it does | If something goes wrong |
|---|---|---|
FileReader(String fileName), FileReader(File file) | Opens the file for reading in the default charset | FileNotFoundException if the file does not exist or is a directory |
FileReader(String fileName, Charset charset) (Java 11) | Same, with an explicit charset | Bytes that are invalid for the charset are replaced with the character � |
FileWriter(String fileName), FileWriter(File file) | Creates the file or truncates an existing one | FileNotFoundException (a subclass of IOException) if the path is a directory, the file is read-only or the parent directory does not exist |
FileWriter(String fileName, boolean append) | With append = true adds to the end of the file instead of overwriting it | If the file does not exist, it is created |
FileWriter(String fileName, Charset charset), FileWriter(String, Charset, boolean append) (Java 11) | Writes with an explicit charset | Characters the charset cannot represent are written as ? |
Before Java 11, FileReader and FileWriter had no Charset parameter, so to read or write text in a specific encoding you had to build a chain with InputStreamReader/OutputStreamWriter (see the next section).
Example 1. Writing and reading a file with FileWriter and FileReader
The streams are closed automatically by try-with-resources. Writing and reading happen in two separate blocks: by the time we read, the FileWriter is already closed and every character is guaranteed to be in the file.
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class FileWriterReaderExample {
public static void main(String[] args) {
File file = new File("fileWrite.txt");
try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) {
writer.write("Learn\nJava!\n");
} catch (IOException e) {
System.out.println("Write error: " + e.getMessage());
}
try (FileReader reader = new FileReader(file, StandardCharsets.UTF_8)) {
char[] buffer = new char[50];
int total = 0;
int count;
while ((count = reader.read(buffer)) != -1) { // read until the end of the file
System.out.print(new String(buffer, 0, count));
total += count;
}
System.out.println("Characters read: " + total);
} catch (IOException e) {
System.out.println("Read error: " + e.getMessage());
}
}
}
// Output:
// Learn
// Java!
// Characters read: 12 Why 12: five characters in Learn, five in Java! and two line feeds \n. If you run only the second half without the file, the FileReader constructor throws FileNotFoundException, and the program prints Read error: fileWrite.txt (No such file or directory) on Linux/macOS (on Windows the reason reads The system cannot find the file specified).
4. InputStreamReader and OutputStreamWriter
InputStreamReader and OutputStreamWriter are the bridge between byte and character streams. InputStreamReader wraps any InputStream and decodes bytes into characters; OutputStreamWriter wraps an OutputStream and encodes characters into bytes. The charset is the second constructor argument — usually StandardCharsets.UTF_8.
new InputStreamReader(InputStream in) // default charset
new InputStreamReader(InputStream in, Charset cs) // explicit charset
new InputStreamReader(InputStream in, String charsetName) // UnsupportedEncodingException for an unknown name
new OutputStreamWriter(OutputStream out)
new OutputStreamWriter(OutputStream out, Charset cs)
new OutputStreamWriter(OutputStream out, String charsetName) The getEncoding() method returns the historical name of the charset the stream uses, for example UTF8 or Cp1252. You need these classes wherever a source gives you bytes but you want to work with text: a file (FileInputStream), a network connection, or the console System.in:
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
String name = console.readLine(); // the line typed on the keyboard How to build a full chain "file → charset → buffer → formatting" from these classes, and why the default charset depended on the OS before Java 18, is shown in the section A chain of wrappers of the I/O streams lesson.
What does this code print?
The program writes the word «café» in UTF-8 and then reads it twice: as windows-1252 and as UTF-8.
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class EncodingDemo {
public static void main(String[] args) throws IOException {
try (Writer out = new OutputStreamWriter(
new FileOutputStream("menu.txt"), StandardCharsets.UTF_8)) {
out.write("café");
}
try (BufferedReader in = new BufferedReader(new InputStreamReader(
new FileInputStream("menu.txt"), Charset.forName("windows-1252")))) {
String line = in.readLine();
System.out.println(line + " (" + line.length() + " characters)");
}
try (BufferedReader in = new BufferedReader(new InputStreamReader(
new FileInputStream("menu.txt"), StandardCharsets.UTF_8))) {
String line = in.readLine();
System.out.println(line + " (" + line.length() + " characters)");
}
}
}
// Output:
// café (5 characters)
// café (4 characters) In UTF-8, é takes two bytes, C3 A9, so the file holds 5 bytes for 4 letters. windows-1252 is a single-byte charset: it turns every byte into a separate character, so C3 becomes à and A9 becomes ©. There is no error and no exception — that is exactly why the charset must be given explicitly on both sides, when writing and when reading.
5. BufferedReader and BufferedWriter
BufferedReader and BufferedWriter are wrappers around another character stream with an internal buffer of 8192 characters. BufferedReader fills the buffer in large blocks and adds readLine() for reading text line by line; BufferedWriter collects characters in the buffer, writes them to the inner stream in blocks and adds newLine().
BufferedReader(Reader in)
BufferedReader(Reader in, int size) // size - buffer size in characters
BufferedWriter(Writer out)
BufferedWriter(Writer out, int size) The first argument is the stream to read from or write to (most often FileReader/FileWriter or InputStreamReader/OutputStreamWriter), the second is the buffer size. The buffer cuts the number of file accesses: instead of a system call per character, the stream goes to the disk once per block.
String readLine()returns the next line without the line terminator (\n,\ror\r\n), andnullwhen the file is over. An empty line in the file comes back as"", notnull.void newLine()writes the line separator of the current OS:\r\non Windows,\non Linux and macOS.Stream<String> lines()(Java 8) returns the lines of the file as a Stream API stream:long n = reader.lines().count();
Example 2. Writing with BufferedWriter and reading with BufferedReader
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class BufferedExample {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("buffered.txt", StandardCharsets.UTF_8))) {
writer.write("Hello, world!");
writer.newLine();
writer.newLine(); // an empty line
writer.write("The end"); // last line without a line separator
} catch (IOException e) {
System.out.println("Write error: " + e.getMessage());
}
try (BufferedReader reader = new BufferedReader(
new FileReader("buffered.txt", StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) { // null - end of file
System.out.println("[" + line + "] length " + line.length());
}
} catch (IOException e) {
System.out.println("Read error: " + e.getMessage());
}
}
}
// Output:
// [Hello, world!] length 13
// [] length 0
// [The end] length 7 The square brackets show that readLine() dropped the line terminators, the empty line came back as "", and the last line was read even without a trailing separator. It is enough to close the outer wrapper: BufferedWriter.close() flushes the buffer and closes the nested FileWriter. Why you should never close the inner stream first is explained in Which stream to close.
Where the line separator comes from
newLine() and println() take the separator from System.lineSeparator(), so a file written on Windows is one byte per line longer than the same file written on Linux. If the file format is fixed (for example, a CSV for another system), write the separator explicitly: writer.write("\n"). Reading is portable — readLine() understands all three variants on any OS.
6. PrintWriter
PrintWriter is a character output stream with convenient print(), println(), printf() and format() methods that turn values of any type into text. It extends Writer and, unlike other streams, does not throw IOException from its writing methods: errors are reported through checkError().
PrintWriter constructors
| Constructor | What it does | Watch out |
|---|---|---|
PrintWriter(String fileName), PrintWriter(File file) | Creates the file or overwrites an existing one; default charset | Throws FileNotFoundException if the file cannot be created or opened |
PrintWriter(String fileName, Charset charset) (Java 10) | Same, with an explicit charset | Declares IOException |
PrintWriter(Writer out), PrintWriter(OutputStream out) | Writes to an existing stream | No automatic flushing: data moves on only on flush(), close() or when a buffer fills up |
PrintWriter(Writer out, boolean autoFlush), PrintWriter(OutputStream out, boolean autoFlush) | With autoFlush = true flushes after every println(), printf() and format() | print() and write() do not flush even with autoFlush |
The file-name constructors cannot append to an existing file — for that, wrap a FileWriter in append mode: new PrintWriter(new FileWriter("log.txt", true)).
PrintWriter methods
print(x)— writes a value of any type as text, without a line separator.println(x)— the same, followed by the OS line separator.printf(format, args...)andformat(format, args...)— identical methods for formatted output with a pattern (%d,%s,%.2f,%n).write(...)— methods inherited fromWriterthat write characters and strings as is.boolean checkError()— flushes the stream and returnstrueif any write has failed.
Example 3. Writing to a file with PrintWriter
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
public class PrintWriterExample {
public static void main(String[] args) {
String s1 = "Welcome to the café!";
String s2 = "Hello World!";
try (PrintWriter printWriter = new PrintWriter("notes.txt", StandardCharsets.UTF_8)) {
printWriter.println(s1);
int i = 2;
printWriter.printf("The square of %d is %d%n", i, i * i);
printWriter.write(s2);
printWriter.print("The end");
} catch (IOException e) {
System.out.println("Cannot open the file: " + e.getMessage());
return;
}
System.out.println("The file has been written");
}
}
// Console output:
// The file has been written The message is printed after the try block, when the PrintWriter is already closed and the data is flushed to the file. Contents of notes.txt:
Welcome to the café!
The square of 2 is 4
Hello World!The end The last line got glued together: neither write() nor print() adds a line separator — only println() and %n in a printf() pattern do. The catch block here catches only a constructor failure: the println()/printf() calls themselves never throw.
The autoFlush catch
If a PrintWriter writes to a socket or to another process, create it with autoFlush = true and send messages with println(). A message sent with print() stays in the buffer, and the other side keeps waiting for a reply that never leaves your program.
7. Character stream gotchas
Most bugs with character streams come from the default charset, overwriting a file instead of appending, buffering, and the fact that PrintWriter stays silent on failure. All of them compile without warnings and show up only on specific data or a specific OS.
| Mistake | What happens | How to do it right |
|---|---|---|
new FileReader("a.txt") without a Charset on Java 17 and earlier | A UTF-8 file on a Western-locale Windows reads as café | new FileReader("a.txt", StandardCharsets.UTF_8) (Java 11+) or InputStreamReader with a Charset |
new FileWriter("log.txt") for a log | The file is truncated on every run, old entries disappear | new FileWriter("log.txt", true) |
while (!(line = reader.readLine()).isEmpty()) | Reading stops at the first empty line, and at the end of the file you get a NullPointerException | The only end-of-file signal is null: while ((line = reader.readLine()) != null) |
new PrintWriter(out, true) followed by print() | The data stays in the buffer: autoFlush works only in println(), printf(), format() | Use println() or call flush() |
Never checking PrintWriter for errors | The disk is full or the stream is closed — no exception, the data is silently lost | Call checkError() after writing, or use BufferedWriter, which throws IOException |
What does this code print?
PrintWriter pw = new PrintWriter(new StringWriter());
pw.close();
pw.println("after close");
System.out.println(pw.checkError()); Answer: the program does not crash and prints true. Writing to a closed PrintWriter internally raises IOException: Stream closed, but println() catches it and only sets an error flag. The same call on a closed BufferedWriter (bw.write("after close")) throws IOException immediately. So PrintWriter is convenient for reports and console-style output, but wherever losing data is unacceptable, check for errors explicitly.
8. Table of character stream classes
The java.io package has about twenty character streams. Almost every one has a byte counterpart (FileReader — FileInputStream, BufferedWriter — BufferedOutputStream), and the classes fall into sources and destinations of data (a file, a string, an array, a pipe) and wrappers that add features to another stream.
| Class | Extends | Purpose |
|---|---|---|
Reader | Object | Abstract class describing a character input stream |
Writer | Object | Abstract class describing a character output stream |
InputStreamReader | Reader | Input stream that converts bytes to characters using a charset |
OutputStreamWriter | Writer | Output stream that converts characters to bytes using a charset |
FileReader | InputStreamReader | Input stream that reads a text file |
FileWriter | OutputStreamWriter | Output stream that writes text to a file |
BufferedReader | Reader | Buffered input stream with readLine() and lines() |
BufferedWriter | Writer | Buffered output stream with newLine() |
LineNumberReader | BufferedReader | Buffered input stream that counts the lines read (getLineNumber()) |
PrintWriter | Writer | Output stream with print(), println(), printf(), format(); does not throw IOException |
StringReader | Reader | Input stream that reads from a String |
StringWriter | Writer | Output stream that collects characters in a string; get the result with toString() |
CharArrayReader | Reader | Input stream that reads from a char[] array |
CharArrayWriter | Writer | Output stream that writes to a growing char[] array (toCharArray()) |
FilterReader | Reader | Abstract base class for filter wrappers around a Reader |
FilterWriter | Writer | Abstract base class for filter wrappers around a Writer |
PushbackReader | FilterReader | Input stream that lets you push characters back (unread()) |
PipedReader | Reader | Input pipe for passing characters between threads |
PipedWriter | Writer | Output pipe connected to a PipedReader |
Byte streams and their classes are covered in the lesson Java Byte Streams. Character streams are not suitable for binary data (images, archives): decoding it with a charset corrupts the file.
Frequently asked questions
Which is better for reading: BufferedReader or Scanner?
BufferedReader is faster for reading large files line by line: an 8192-character buffer and no parsing. Scanner is more convenient when you need to parse numbers and words (nextInt(), nextDouble()), but it is slower because it relies on regular expressions, and it does not throw IOException — the last error is available through ioException(). Reading keyboard input with Scanner is covered in Java User Input with System.in and Scanner.
How do I read a whole text file into a String?
Since Java 11 it is one line: String text = Files.readString(Path.of("a.txt")); (UTF-8 by default; if the file contains bytes that are invalid in UTF-8, you get MalformedInputException). With character streams, use reader.lines().collect(Collectors.joining("\n")) on a BufferedReader. For gigabyte-sized files, read line by line instead of loading everything into memory.
How do I get an exception stack trace as a String?
Combine StringWriter and PrintWriter: StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); String trace = sw.toString();. The printStackTrace(PrintWriter) method writes the trace to any Writer, and StringWriter collects it in memory. You do not have to close the StringWriter — its close() does nothing.
What is the difference between PrintWriter and PrintStream (System.out)?
Their methods are almost the same, and neither throws IOException. The difference is the hierarchy: PrintStream is a byte stream (it extends OutputStream), encodes characters into bytes itself and can also write raw bytes; PrintWriter is a character stream (it extends Writer) and can wrap any Writer. System.out and System.err are PrintStream objects.
Comments