FileReader and FileWriter are character streams for reading/writing text files in Java.
FileWriter overwrites by default; pass true for append mode to avoid silent data loss.
Always wrap with BufferedReader/BufferedWriter — raw streams make one OS syscall per character.
Platform default charset can corrupt non-ASCII data; use InputStreamReader/OutputStreamWriter with UTF-8.
Always use try-with-resources to guarantee flush and close — missing close loses data silently.
Use newLine() for platform-independent line separators, not hardcoded "\n".
Plain-English First
Imagine your Java program is a person sitting at a desk. FileReader is like that person picking up a physical letter from a folder and reading it word by word. FileWriter is like that same person picking up a pen and writing a new letter into a folder. The 'file' on disk is the folder, and your Java program is the person doing the reading or writing. Simple as that.
Every serious application eventually needs to talk to the file system — reading config files, writing logs, importing CSV data, exporting reports. Java's FileReader and FileWriter are the most direct tools for doing exactly that with text files. They're part of the java.io package, which has been in Java since version 1.1, and understanding them properly unlocks a whole layer of practical programming that goes beyond printing to the console.
The problem they solve is straightforward: your program's memory is temporary. The moment your JVM shuts down, everything in RAM is gone. FileWriter lets you persist text data to disk so it survives restarts, reboots, and crashes. FileReader is the flip side — it lets you pull that saved data back into memory so your program can work with it again. Together they form the foundation of text-based file I/O in Java.
By the end of this article you'll know how FileReader and FileWriter work under the hood, how to use them safely with try-with-resources, how to append instead of overwrite, how to read files efficiently character by character or line by line, and exactly which real-world situations call for them versus their more powerful alternatives. You'll also know the three mistakes that trip up most intermediate developers — and how to avoid them entirely.
What FileWriter Actually Does — Writing Text to Disk
FileWriter is a character stream writer. That means it converts Java characters (which are Unicode) into bytes and writes them to a file. By default it uses the platform's default charset — more on why that matters in the pitfalls section.
When you create a FileWriter with just a filename, it opens the file in 'overwrite' mode. Every run wipes the file clean and starts fresh. If you pass true as the second argument, it switches to 'append' mode — new content goes to the end of the existing file. This is how log files work in most basic applications.
FileWriter extends OutputStreamWriter, which extends Writer. So it's fully polymorphic — anywhere you need a Writer, a FileWriter fits. This matters because it means you can wrap it with a BufferedWriter for dramatically better performance. Raw FileWriter hits the disk on every single write() call. BufferedWriter batches those writes into chunks. For anything longer than a few lines, always wrap.
You must close a FileWriter when you're done. Failing to do so is one of the most common bugs — the data never actually reaches the disk because it's still sitting in an internal buffer waiting to be flushed. The safest way to guarantee the file gets closed is try-with-resources, which Java handles automatically.
WriteUserReport.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
publicclassWriteUserReport {
publicstaticvoidmain(String[] args) {
String reportFilePath = "user_report.txt";
// Try-with-resources ensures the writer is closed automatically,// even if an exception is thrown mid-write.// BufferedWriter wraps FileWriter for performance — without it,// every write() call is a separate OS-level disk operation.try (BufferedWriter reportWriter = newBufferedWriter(newFileWriter(reportFilePath))) {
// Write the report header
reportWriter.write("=== Monthly User Report ===");
reportWriter.newLine(); // platform-safe newline (\r\n on Windows, \n on Unix)
reportWriter.write("Username: alice_dev");
reportWriter.newLine();
reportWriter.write("Logins this month: 47");
reportWriter.newLine();
reportWriter.write("Status: Active");
reportWriter.newLine();
// No need to call flush() or close() — try-with-resources does itSystem.out.println("Report written successfully to: " + reportFilePath);
} catch (IOException writeException) {
// IOException covers: file not found, permission denied, disk full, etc.System.err.println("Failed to write report: " + writeException.getMessage());
}
}
}
Output
Report written successfully to: user_report.txt
[user_report.txt contents]
=== Monthly User Report ===
Username: alice_dev
Logins this month: 47
Status: Active
Pro Tip: Always Use newLine() Over "\n"
Hardcoding "\n" in a FileWriter will produce Unix-style line endings on every platform — which breaks files opened in Notepad on Windows. BufferedWriter.newLine() writes the correct line separator for whatever OS is running. Use it every time.
Production Insight
Without BufferedWriter, each write() call is a separate disk write.
For a 10KB file, that's thousands of syscalls vs one chunk.
In production, raw FileWriter can cause 100x slowdowns on high-throughput logging.
Rule: always wrap FileWriter in BufferedWriter unless you're writing a single small line.
Key Takeaway
Always wrap FileWriter in BufferedWriter.
Raw FileWriter hits disk per character — unacceptable for production.
Use try-with-resources to guarantee flush and close.
Choosing Between Raw FileWriter and BufferedWriter
IfWriting less than 100 characters total
→
UseRaw FileWriter works, but wrapping adds negligible overhead. Wrap anyway for consistency.
IfWriting more than 100 characters or multiple lines
→
UseAlways use BufferedWriter. Performance difference grows with file size.
IfWriting to a slow filesystem (NFS, network drive)
→
UseBufferedWriter is mandatory — reduce syscalls to avoid latency spikes.
Appending to a File — The Second Argument That Changes Everything
The most common gotcha with FileWriter is accidentally nuking an existing file. If you're building a logger, an audit trail, or any kind of running history, you need append mode. The fix is a single boolean argument: new FileWriter(filePath, true). That true tells Java to open the file at the end rather than from the beginning.
Under the hood, true maps to the FileOutputStream append flag, which maps to the OS-level open call with O_APPEND. This means even if two processes try to append to the same file simultaneously, the OS handles the ordering — though for true concurrent logging in production you'd use a dedicated logging framework.
The pattern below simulates a simple application event log — each time the program runs, it adds a new timestamped entry without touching anything already in the file. This is exactly how application logs, audit trails, and event histories are built at a basic level.
AppendEventLog.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
publicclassAppendEventLog {
// Centralized log path — in a real app this comes from configprivatestaticfinalString LOG_FILE_PATH = "application_events.log";
privatestaticfinalDateTimeFormatter TIMESTAMP_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
publicstaticvoidlogEvent(String eventMessage) throwsIOException {
// The 'true' argument is the key — it enables append mode.// Without it, every call to logEvent() would destroy the previous log.try (BufferedWriter logWriter = newBufferedWriter(newFileWriter(LOG_FILE_PATH, true))) {
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
// Format: [2024-06-15 14:32:01] User alice_dev logged in
logWriter.write("[" + timestamp + "] " + eventMessage);
logWriter.newLine();
}
// Writer is closed here — the OS commits this line to disk
}
publicstaticvoidmain(String[] args) throwsIOException {
// Simulate three events happening across one sessionlogEvent("Application started");
logEvent("User alice_dev logged in");
logEvent("User alice_dev exported report");
System.out.println("Events logged. Check: " + LOG_FILE_PATH);
}
}
Output
Events logged. Check: application_events.log
[application_events.log contents after first run]
[2024-06-15 14:32:01] Application started
[2024-06-15 14:32:01] User alice_dev logged in
[2024-06-15 14:32:01] User alice_dev exported report
[application_events.log contents after second run — old entries still there]
[2024-06-15 14:32:01] Application started
[2024-06-15 14:32:01] User alice_dev logged in
[2024-06-15 14:32:01] User alice_dev exported report
[2024-06-15 14:35:44] Application started
[2024-06-15 14:35:44] User alice_dev logged in
[2024-06-15 14:35:44] User alice_dev exported report
Watch Out: Forgetting the Append Flag Deletes Your Data
new FileWriter("mylog.log") with no second argument will silently erase your entire log file every time the program starts. This is a zero-error, zero-warning data loss bug. Always be explicit: pass true for append, or false for overwrite — never rely on the default when it matters.
Production Insight
Append flag is atomic at OS level — two concurrent writers won't corrupt each other's data.
But if one writer opens without append, it truncates the file and the other writer loses its content.
Rule: enforce append flag in code review and config validation.
Key Takeaway
Mixing up append and overwrite causes silent data loss.
Explicitly pass true or false — never rely on default.
For logs, audit trails, and history: append mode is your friend.
UseAlways use append mode: new FileWriter(path, true).
IfExporting a new report each time (e.g., daily CSV dump)
→
UseOverwrite mode is correct. Use new FileWriter(path, false) or default.
IfRunning in a container with ephemeral storage
→
UseStill use append if you want multiple runs preserved; but consider volume mounting.
Reading Files With FileReader — Character by Character and Line by Line
FileReader is the reading counterpart. Like FileWriter, it's a character stream — it reads bytes from disk and converts them to Java chars using the platform's default charset. Wrapping it with BufferedReader is not optional for real code. BufferedReader adds a read buffer (8KB by default) so Java isn't making a system call to the OS for every single character, and it provides the essential readLine() method.
readLine() returns the next line of text without the line terminator, or null when the file ends. That null check in the while loop is the idiomatic Java pattern for reading a file line by line. Forgetting that null signals EOF (end of file) and not an error is a classic beginner mistake.
The example below reads a simple CSV-style config file — the kind you'd use to store database connection settings or feature flags. It parses each line into a key-value pair, demonstrating a real use case rather than just printing raw file contents.
ReadConfigFile.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
publicclassReadConfigFile {
// Assume this file exists on disk with the content shown in the output sectionprivatestaticfinalString CONFIG_FILE_PATH = "app_config.properties";
publicstaticMap<String, String> loadConfig(String filePath) throwsIOException {
Map<String, String> configSettings = newHashMap<>();
// BufferedReader wraps FileReader — gives us readLine() and a memory buffer// so we're not hitting the disk character-by-charactertry (BufferedReader configReader = newBufferedReader(newFileReader(filePath))) {
String currentLine;
// readLine() returns null at end-of-file — NOT an empty string// This while loop is the standard Java pattern for reading all lineswhile ((currentLine = configReader.readLine()) != null) {
// Skip blank lines and comment lines (lines starting with #)if (currentLine.isBlank() || currentLine.startsWith("#")) {
continue;
}
// Each line is expected to look like: db.host=localhostString[] keyValuePair = currentLine.split("=", 2);
if (keyValuePair.length == 2) {
String settingKey = keyValuePair[0].trim();
String settingValue = keyValuePair[1].trim();
configSettings.put(settingKey, settingValue);
}
}
}
// BufferedReader (and the FileReader inside it) closed automatically herereturn configSettings;
}
publicstaticvoidmain(String[] args) {
try {
Map<String, String> appConfig = loadConfig(CONFIG_FILE_PATH);
System.out.println("Loaded " + appConfig.size() + " config settings:");
appConfig.forEach((key, value) ->
System.out.println(" " + key + " -> " + value)
);
} catch (IOException configLoadException) {
System.err.println("Could not load config: " + configLoadException.getMessage());
}
}
}
Output
[app_config.properties file contents]
# Database configuration
db.host=localhost
db.port=5432
db.name=forge_production
# Feature flags
feature.dark_mode=true
[Console output when running ReadConfigFile]
Loaded 4 config settings:
db.host -> localhost
db.port -> 5432
db.name -> forge_production
feature.dark_mode -> true
Interview Gold: Why Wrap FileReader With BufferedReader?
FileReader.read() makes one system call per character — that's potentially thousands of OS calls for a 10KB file. BufferedReader reads a chunk (8192 chars by default) into memory in one call, then serves your code from that buffer. This can be 10-100x faster on real hardware. This answer alone separates candidates who've actually used I/O from those who've just read about it.
Production Insight
readLine() returns null at EOF — not an empty string.
A common bug: using while ((line = reader.readLine()) != null) is correct, but many novices write while (!line.isEmpty()) and miss the last line.
Rule: always check for null, never treat empty string as EOF.
Key Takeaway
Always wrap FileReader in BufferedReader for performance and readLine().
readLine() returns null at EOF — never treat empty string as end.
For small files, consider Files.readString() (Java 11+).
Which Reading Method to Use?
IfFile is less than 100 lines and you need all content as a string
→
UseUse Files.readString(path) (Java 11+) — simpler, no looping.
IfFile is large ( > 1MB ) or you need to process line by line
IfYou need fine-grained control over reading (e.g., skip bytes)
→
UseUse InputStreamReader or NIO channels; FileReader is too high-level.
Copying a Text File — Putting FileReader and FileWriter Together
The clearest way to understand both classes working together is to build a file copy utility. This is also a surprisingly common real-world task — think copying templates, creating backup files, or duplicating config files before modifying them.
This example reads from a source file line by line and writes each line to a destination file, preserving the structure. It also adds a metadata header to the copy — something a raw Files.copy() call couldn't do without extra steps.
Notice the try-with-resources block manages both the reader and the writer simultaneously. When the block exits — successfully or via exception — both streams are closed in reverse declaration order (writer first, then reader). This is Java's guaranteed cleanup contract, and it's the only safe way to handle multiple I/O resources together.
TextFileCopier.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDate;
publicclassTextFileCopier {
/**
* Copies a text file from sourcePath to destinationPath,
* prepending a metadata header to the copy.
*
* @param sourcePath path to the original text file
* @param destinationPath path where the copy will be written
* @throwsIOExceptionif either file cannot be opened or written
*/
publicstaticvoidcopyWithMetadata(String sourcePath, String destinationPath)
throwsIOException {
// Both resources declared in one try-with-resources block —// Java closes them both automatically, in reverse order (writer, then reader)try (
BufferedReader sourceReader = newBufferedReader(newFileReader(sourcePath));
BufferedWriter destinationWriter = newBufferedWriter(newFileWriter(destinationPath))
) {
// Add a metadata header that doesn't exist in the original
destinationWriter.write("# Copied from: " + sourcePath);
destinationWriter.newLine();
destinationWriter.write("# Copy date: " + LocalDate.now());
destinationWriter.newLine();
destinationWriter.write("# ----------------------------------------");
destinationWriter.newLine();
String sourceLine;
int lineCount = 0;
// Read source line by line — null signals we've reached the endwhile ((sourceLine = sourceReader.readLine()) != null) {
destinationWriter.write(sourceLine);
destinationWriter.newLine();
lineCount++;
}
System.out.println("Copy complete. Lines copied: " + lineCount);
System.out.println("Destination: " + destinationPath);
}
// Both streams flushed and closed here — content is safely on disk
}
publicstaticvoidmain(String[] args) {
try {
copyWithMetadata("original_template.txt", "template_backup_copy.txt");
} catch (IOException copyException) {
System.err.println("File copy failed: " + copyException.getMessage());
}
}
}
Output
Copy complete. Lines copied: 12
Destination: template_backup_copy.txt
[template_backup_copy.txt first 3 lines]
# Copied from: original_template.txt
# Copy date: 2024-06-15
# ----------------------------------------
[... rest of original file content follows ...]
Pro Tip: Multiple Resources in One Try-With-Resources
Separate multiple AutoCloseable resources with semicolons inside the try() parentheses. They close in reverse declaration order — always. This means if you have a reader feeding a writer, the writer closes first (flushing its buffer to disk) before the reader closes. Declare them in the order you open them and let Java handle the rest.
Production Insight
An IOException while reading after writing has started leaves a partial destination file.
The try-with-resources ensures the writer is closed (and flushed) even if the read fails.
But the partial file will exist. For critical copies, use a temp file + rename pattern.
Rule: for production file copies, write to a temp file first, then atomically rename.
Key Takeaway
Use try-with-resources for multiple streams — declared in open order, closed in reverse.
Writer closes first, flushing data to disk, then reader closes.
For production, prefer temp-file + rename for atomicity.
This is the most invisible, hardest-to-debug bug in Java I/O because the code compiles, runs, and produces output — it's just the wrong output. No exception is thrown. The only way to detect it is to inspect the raw bytes or open the file on a different platform.
The fix is straightforward: never use raw FileReader/FileWriter when your content might contain non-ASCII characters. Instead, use InputStreamReader wrapping a FileInputStream, and OutputStreamWriter wrapping a FileOutputStream. Both accept an explicit charset parameter.
The example below shows how to write and read a file with UTF-8 encoding, guaranteeing the same result on any platform.
Read back: Pièce de résistance — café au lait: €5,00 éñçödë
Characters match: true
[If you run this on any OS, the output is identical.]
Critical: Raw FileReader/FileWriter Are Not Cross-Platform Safe
If your application runs on multiple OSes (developer laptops + Linux servers), raw FileReader/FileWriter will produce different byte streams on each. Always use InputStreamReader/OutputStreamWriter with an explicit charset like StandardCharsets.UTF_8 to guarantee consistency. This is the #1 cause of 'works on my machine' bugs in file I/O.
Production Insight
A service that processed French customer names (with accented characters) returned garbage after moving from dev Windows to prod Linux.
Root cause: FileReader used default charset — different on each platform.
Fix: switched to InputStreamReader with UTF-8. No more data corruption.
Rule: never trust default charset for files that leave the machine.
Key Takeaway
Default charset differs per platform — causes silent data corruption.
For any non-ASCII content, use InputStreamReader/OutputStreamWriter with UTF-8.
Test on all deployment platforms to catch charset bugs early.
Charset Selection for File I/O
IfFile contains only ASCII characters (A-Z, 0-9, basic symbols)
→
UseFileReader/FileWriter are safe — default charset won't affect ASCII.
IfFile contains accented characters, emojis, or any non-ASCII
→
UseUse InputStreamReader/OutputStreamWriter with explicit StandardCharsets.UTF_8.
IfFile needs to be compatible with legacy systems (e.g., ISO-8859-1)
→
UseUse explicit charset matching the legacy system, not the platform default.
● Production incidentPOST-MORTEMseverity: high
Production Incident: Log File Goes Missing After Server Restart
Symptom
After a routine nightly server restart, all new transaction log entries for the next 3 hours were written to a fresh file — the previous day's logs were silently deleted. Operations noticed when a compliance audit failed to find entries for that window.
Assumption
The team assumed that since the log writer was called daily, it would continue appending to the existing file. They had not reviewed the writer's constructor for years.
Root cause
The FileWriter was initialized as new FileWriter("transactions.log") without the second boolean parameter. This default mode overwrites the file every time the JVM starts. After the server restart, the old file contents were replaced with new data.
Fix
Changed to new FileWriter("transactions.log", true) to enable append mode. Also added a health check that validates the file still contains expected entries after restart.
Key lesson
Always explicitly specify append mode (true) for any persistent log, audit trail, or incremental output.
Treat file writer initialization as a configuration review item during release checklists.
Add monitoring to detect sudden file truncation — e.g., compare expected line count with actual.
Production debug guideCommon symptoms and diagnostic actions for file I/O failures.5 entries
Symptom · 01
File is empty or contains only partial data after write
→
Fix
Check if the FileWriter was closed. Use try-with-resources. If closed manually, ensure close() is in a finally block. Check disk space and permissions.
Symptom · 02
Previously written data is missing on next run
→
Fix
Verify FileWriter constructor: new FileWriter(path) overwrites. Use new FileWriter(path, true) for append. Check if a file deletion occurs elsewhere.
Symptom · 03
Non-ASCII characters appear as '?' or garbage
→
Fix
Confirm charset: FileReader/FileWriter use platform default. Rewrite with InputStreamReader/OutputStreamWriter specifying StandardCharsets.UTF_8.
Symptom · 04
Very slow read/write on files larger than a few KB
→
Fix
Check if BufferedReader/BufferedWriter is used. Raw reader/writer makes one OS syscall per character. Wrap immediately.
Symptom · 05
readLine() returns empty string for blank lines, but stops prematurely
→
Fix
Remember readLine() returns null at EOF, not an empty string. A blank line returns "". Ensure loop checks line != null, not !line.isEmpty().
★ Quick Debug Cheat Sheet: File I/O FailuresImmediate actions and commands when your file reads/writes misbehave.
Data not written to file−
Immediate action
Check if FileWriter was closed. Check disk space using `df -h` (Linux) or `dir` (Windows). Verify file path existence.
Commands
ls -la /path/to/file (Linux) or dir C:\path\to\file (Windows) — check file size and permissions
cat /path/to/file (Linux) or type C:\path\to\file (Windows) — view file contents immediately
Fix now
Close the writer properly. If using try-with-resources, ensure the resource is declared inside the try(). For manual close, add finally block.
Garbled characters or '?' in output+
Immediate action
Check file encoding with `file -i filename` (Linux) or note charset mismatch. Stop using raw FileWriter.
Commands
file -i /path/to/file (Linux) — shows charset of written file
Replace FileWriter with OutputStreamWriter wrapped around FileOutputStream, specifying StandardCharsets.UTF_8. Same for reading: InputStreamReader + FileInputStream with UTF-8.
File content is from previous run, not current+
Immediate action
Check if you used append mode. Look at FileWriter constructor — missing second argument means overwrite.
Commands
Inspect source code: search for `new FileWriter` and verify the boolean flag. If not present, add `true`.
Run a quick test: create a writer with `true` and write a marker line, then restart app and verify marker still exists.
Fix now
Change constructor to new FileWriter(path, true). Also consider using StandardOpenOption.APPEND if using Files.newBufferedWriter.
FileReader/FileWriter vs NIO Files Methods
Feature / Aspect
FileReader / FileWriter
Files.readString / Files.writeString (NIO)
Java version introduced
Java 1.1
Java 11+
Reading entire file
Requires loop + StringBuilder
One method call
Writing entire string
Requires open, write, close
One method call
Charset control
Only via InputStreamReader wrapper
Built-in Charset parameter
Best for
Streaming large files line by line
Small files, quick reads/writes
Buffering needed?
Yes — always wrap with Buffered*
Built in automatically
Append mode
new FileWriter(path, true)
StandardOpenOption.APPEND flag
Performance on large files
Excellent when buffered
Loads whole file into memory
Exception type
Checked IOException
Checked IOException
Closing resources
Must use try-with-resources
Handled internally
Key takeaways
1
FileWriter has two modes
overwrite (default) and append (pass true as the second argument) — getting this wrong causes silent data loss with no exception or warning.
2
Always wrap FileReader in BufferedReader and FileWriter in BufferedWriter
raw FileReader/FileWriter make one OS system call per character, which kills performance on anything larger than a few lines.
3
Try-with-resources is non-negotiable
if you don't close a FileWriter, its internal buffer may never flush to disk, meaning your data never actually gets written even though no exception was thrown.
4
FileReader and FileWriter inherit the platform's default charset
for any file with non-ASCII content, switch to InputStreamReader/OutputStreamWriter with an explicit StandardCharsets.UTF_8 argument to avoid cross-platform encoding bugs.
5
For very small files (under ~10MB), consider using the Files.readString() and Files.writeString() methods from Java NIO (11+)
they are simpler, charset-safe, and automatically manage resources.
Common mistakes to avoid
4 patterns
×
Not wrapping FileReader/FileWriter with their Buffered counterparts
Symptom
Code works correctly but is extremely slow on files larger than a few KB because every character read/write is a separate OS system call.
Fix
Always write new BufferedReader(new FileReader(path)) and new BufferedWriter(new FileWriter(path)); never use FileReader or FileWriter raw in production code.
×
Forgetting to close the stream (or not using try-with-resources)
Symptom
Data written with FileWriter appears missing or truncated in the output file, because the internal buffer was never flushed to disk.
Fix
Always use try-with-resources; if you must close manually, put writer.close() in a finally block, never just at the end of the try block where an exception could skip it.
Use new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8) and new OutputStreamWriter(new FileOutputStream(path), StandardCharsets.UTF_8) instead of raw FileReader/FileWriter whenever your content might contain non-ASCII text.
×
Mistaking append mode for overwrite (or vice versa)
Symptom
Log files or incremental outputs are erased every time the program starts, or new data appears unexpectedly on top of old data.
Fix
Explicitly pass true for append mode: new FileWriter(path, true). For overwrite, pass false or use the one-arg constructor but document the intent. Never assume the default is correct.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Why should you always wrap FileReader with BufferedReader rather than us...
Q02SENIOR
What is the difference between new FileWriter('log.txt') and new FileWri...
Q03SENIOR
FileReader and FileWriter use the platform's default charset. Why is thi...
Q04SENIOR
Under what circumstances would you choose Files.readString() over FileRe...
Q01 of 04SENIOR
Why should you always wrap FileReader with BufferedReader rather than using FileReader directly? What exactly happens at the OS level if you don't?
ANSWER
FileReader.read() makes a system call to the OS for every single character. A 50KB file results in ~50,000 syscalls. BufferedReader reads an 8KB chunk into memory in one call, then serves subsequent reads from that buffer without touching the OS. This can be 10-100x faster and also provides the essential readLine() method.
Q02 of 04SENIOR
What is the difference between new FileWriter('log.txt') and new FileWriter('log.txt', true), and what real-world bug does confusing them cause?
ANSWER
The first opens the file in overwrite mode (default) — it truncates the file to zero length and writes from the beginning. The second opens the file in append mode — it positions the file pointer at the end and all writes go there. Confusing them causes silent data loss: log files are overwritten without warning, audit trails disappear, and it can take hours to detect.
Q03 of 04SENIOR
FileReader and FileWriter use the platform's default charset. Why is this a problem, and how would you rewrite them to guarantee UTF-8 encoding in a cross-platform application?
ANSWER
The platform's default charset varies between OS and locale. A file written on Windows (Cp1252) may be unreadable on Linux (UTF-8) — accented characters become garbage. To guarantee UTF-8, replace FileReader with InputStreamReader wrapped around FileInputStream, and FileWriter with OutputStreamWriter wrapped around FileOutputStream, passing StandardCharsets.UTF_8 as the charset argument.
Q04 of 04SENIOR
Under what circumstances would you choose Files.readString() over FileReader, and vice versa?
ANSWER
Files.readString() (Java 11+) is ideal for small files (under ~10MB) you want to load entirely into memory — it's concise, charset-safe, and handles close automatically. Use FileReader with BufferedReader when processing large files line by line to avoid OutOfMemoryErrors, or when you need to apply custom parsing logic per line.
01
Why should you always wrap FileReader with BufferedReader rather than using FileReader directly? What exactly happens at the OS level if you don't?
SENIOR
02
What is the difference between new FileWriter('log.txt') and new FileWriter('log.txt', true), and what real-world bug does confusing them cause?
SENIOR
03
FileReader and FileWriter use the platform's default charset. Why is this a problem, and how would you rewrite them to guarantee UTF-8 encoding in a cross-platform application?
SENIOR
04
Under what circumstances would you choose Files.readString() over FileReader, and vice versa?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the difference between FileReader and BufferedReader in Java?
FileReader is the low-level stream that connects directly to a file on disk and reads one character at a time via OS system calls. BufferedReader is a wrapper that sits on top of FileReader and stores a chunk of characters in memory (8KB by default), dramatically reducing the number of OS calls. BufferedReader also adds the essential readLine() method. You almost always use both together: new BufferedReader(new FileReader(path)).
Was this helpful?
02
Does Java FileWriter create the file if it doesn't exist?
Yes — if the file doesn't exist, FileWriter creates it automatically. However, it will throw a FileNotFoundException (which is a subclass of IOException) if any parent directory in the path doesn't exist. So new FileWriter('reports/june/output.txt') will fail if the 'reports/june/' directory hasn't been created yet. Use new File('reports/june/').mkdirs() before writing if you need to guarantee the directory exists.
Was this helpful?
03
When should I use FileReader/FileWriter versus Java NIO Files.readString or Files.writeString?
Use Files.readString() and Files.writeString() (available since Java 11) when dealing with small files you want to read or write in a single operation — it's simpler, handles charset properly, and requires less code. Use FileReader and FileWriter (wrapped in their Buffered counterparts) when streaming large files line by line, because NIO's single-call methods load the entire file into memory at once, which can cause OutOfMemoryErrors on multi-gigabyte files.
Was this helpful?
04
What happens if I forget to close a FileWriter?
The internal buffer may never be flushed to disk, which means the data you think you wrote is actually still sitting in memory and is lost when the program ends. No exception is thrown. This is why try-with-resources is the standard approach — it guarantees close() (and hence flush()) is called even if an exception occurs.
Was this helpful?
05
Can I use FileReader and FileWriter for binary files?
No. FileReader and FileWriter are character streams designed for text. They attempt to decode bytes into characters using a charset, which corrupts binary data. For binary files (images, PDFs, serialized objects), use FileInputStream and FileOutputStream directly.