File Handling and I/O
Writing and Updating Text Files
You are viewing a free preview lesson.
Lesson Overview
আগের lesson-এ আমরা text file পড়েছি।
এখন আমরা শিখব কীভাবে Java দিয়ে text file:
- Create করা যায়
- Overwrite করা যায়
- Append করা যায়
- Lines হিসেবে লেখা যায়
- UTF-8 encoding ব্যবহার করা যায়
- Parent directory ensure করা যায়
- Safer update strategy ব্যবহার করা যায়
Modern Java-তে common APIs:
Files.writeString()
Files.write()
Files.createDirectories()
Files.move()
এই lesson-এ আমরা বিশেষভাবে focus করব:
Small text files
Markdown content
Generated reports
Configuration exports
Course content storage
Learning Objectives
এই lesson শেষে আপনি পারবেন:
Files.writeString()দিয়ে text লিখতে- Existing file overwrite করতে
- New file create করতে
- Append mode ব্যবহার করতে
StandardOpenOptionবুঝতেFiles.write()দিয়ে lines লিখতে- Parent directories তৈরি করতে
- UTF-8 explicitly ব্যবহার করতে
- Write failures translate করতে
- Temporary file + replacement strategy explain করতে
Basic File Writing
Suppose:
Path path =
Path.of(
"course.md"
);
Write:
Files.writeString(
path,
"# Java Course"
);
If successful, file contains:
# Java Course
Files.writeString()
Common signature:
Files.writeString(
path,
content
);
It writes a String to a file.
Example:
String content =
"""
# Java
Learn Java the right way.
""";
Files.writeString(
path,
content
);
Explicit UTF-8
Prefer:
Files.writeString(
path,
content,
StandardCharsets.UTF_8
);
Import:
import java.nio.charset.StandardCharsets;
This makes the storage encoding explicit.
What Happens If the File Does Not Exist?
With the normal writeString() behavior, Java usually creates the file.
Example:
Path path =
Path.of(
"new-course.md"
);
Files.writeString(
path,
"Java"
);
If the parent directory exists, the file is created.
What Happens If the File Already Exists?
By default:
Files.writeString(
path,
content
);
replaces the existing content.
Example existing file:
Old content
Write:
Files.writeString(
path,
"New content"
);
Result:
New content
Overwrite Is Destructive
This is important:
Files.writeString(...)
does not automatically preserve old content.
If existing file contains:
Lesson 1
Lesson 2
Lesson 3
and you write:
Updated lesson
the old content is replaced.
Before choosing overwrite, understand the operation contract.
StandardOpenOption
Java allows more explicit write behavior through:
StandardOpenOption
Import:
import java.nio.file.StandardOpenOption;
Common options:
CREATE
CREATE_NEW
WRITE
APPEND
TRUNCATE_EXISTING
CREATE
StandardOpenOption.CREATE
Meaning:
Create the file if it does not exist.
If it already exists, Java can still open it depending on other options.
CREATE_NEW
StandardOpenOption.CREATE_NEW
Meaning:
Create only if the file does not exist.
If file already exists:
FileAlreadyExistsException
is thrown.
WRITE
StandardOpenOption.WRITE
opens the file for writing.
Usually combined with other options.
TRUNCATE_EXISTING
StandardOpenOption.TRUNCATE_EXISTING
means:
If the file exists, clear its current content before writing.
Example:
Files.writeString(
path,
content,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE
);
This explicitly expresses overwrite behavior.
APPEND
StandardOpenOption.APPEND
adds content to the end of the existing file.
Example:
Files.writeString(
path,
"New line\n",
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
);
If file contains:
Java
result becomes:
Java
New line
assuming the previous content ended with a line break or you added one deliberately.
Append Does Not Add a New Line Automatically
This:
Files.writeString(
path,
"Spring",
StandardCharsets.UTF_8,
StandardOpenOption.APPEND
);
does not automatically insert:
\n
If file contains:
Java
the result may become:
JavaSpring
If you want a new line:
Files.writeString(
path,
System.lineSeparator()
+ "Spring",
StandardCharsets.UTF_8,
StandardOpenOption.APPEND
);
System.lineSeparator()
Instead of hardcoding:
"\n"
you can use:
System.lineSeparator()
It represents the platform line separator.
For many application file formats, especially controlled text formats, using \n is also common.
The important thing is consistency.
Create Only If Missing
Suppose course content should never overwrite an existing file accidentally.
Use:
Files.writeString(
path,
content,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE_NEW
);
If file exists:
FileAlreadyExistsException
is thrown.
Handling FileAlreadyExistsException
try {
Files.writeString(
path,
content,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE_NEW
);
} catch (
FileAlreadyExistsException exception
) {
throw new IllegalStateException(
"Course content already exists.",
exception
);
} catch (
IOException exception
) {
throw new IllegalStateException(
"Could not write course content.",
exception
);
}
Specific exception first.
Creating Parent Directories
Suppose target is:
content/java-oop/module-1/lesson.md
If:
content/java-oop/module-1
does not exist, file writing may fail.
Create parents first:
Path parent =
path.getParent();
if (parent != null) {
Files.createDirectories(
parent
);
}
createDirectories()
Files.createDirectories(
path
);
creates all missing directories in the path.
Example:
content/java-oop/module-1
If:
content
exists but:
java-oop/module-1
does not, Java creates the missing levels.
createDirectory() vs createDirectories()
createDirectory()
Creates one directory.
Its parent must already exist.
createDirectories()
Creates all missing parent directories as needed.
For application storage paths, createDirectories() is often more practical.
Parent May Be null
For:
Path.of(
"course.md"
)
calling:
path.getParent()
may return:
null
So:
Files.createDirectories(
path.getParent()
);
could fail.
Use:
Path parent =
path.getParent();
if (parent != null) {
Files.createDirectories(
parent
);
}
Writing Multiple Lines
Java also provides:
Files.write(...)
Example:
List<String> lines =
List.of(
"Java",
"Spring",
"Kafka"
);
Files.write(
path,
lines,
StandardCharsets.UTF_8
);
This writes lines to the file.
Files.write() vs writeString()
Use:
writeString()
when content is naturally one text document.
Use:
write()
when you already have:
Iterable<String>
or:
List<String>
Example:
Course Markdown → writeString()
Generated list of topics → write()
Writing a Markdown Lesson
String markdown =
"""
# Introduction to Java
Java is a strongly typed programming language.
""";
Files.writeString(
Path.of(
"content",
"java-oop",
"introduction.md"
),
markdown,
StandardCharsets.UTF_8
);
Safe Storage Method
A reusable method:
public static void writeText(
Path path,
String content
) {
if (path == null) {
throw new IllegalArgumentException(
"Path is required."
);
}
if (content == null) {
throw new IllegalArgumentException(
"Content is required."
);
}
try {
Path parent =
path.getParent();
if (parent != null) {
Files.createDirectories(
parent
);
}
Files.writeString(
path,
content,
StandardCharsets.UTF_8
);
} catch (
IOException exception
) {
throw new IllegalStateException(
"Could not write text file.",
exception
);
}
}
Validate Before Writing
Weak:
Files.writeString(
path,
content
);
if (content.isBlank()) {
throw new IllegalArgumentException(
"Content cannot be blank."
);
}
The invalid content has already been written.
Better:
if (
content == null
|| content.isBlank()
) {
throw new IllegalArgumentException(
"Content is required."
);
}
Files.writeString(
path,
content
);
Validate before side effects.
Decide Whether Empty Content Is Valid
Do not assume:
Empty file = invalid
Some use cases allow empty files.
Example:
Placeholder file
Empty generated export
Optional content
Other cases may require non-blank content.
The domain contract decides.
Appending Course Activity
Suppose you want a simple audit file:
course-events.log
Append:
public static void appendEvent(
Path path,
String event
) {
try {
Path parent =
path.getParent();
if (parent != null) {
Files.createDirectories(
parent
);
}
Files.writeString(
path,
event
+ System.lineSeparator(),
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
);
} catch (
IOException exception
) {
throw new IllegalStateException(
"Could not append course event.",
exception
);
}
}
Append Is Not a Database
A text file append strategy may work for:
- Simple local logs
- Development tools
- Small exports
But it should not automatically be treated as a replacement for:
Database
Durable event log
Transactional storage
Concurrent writers and consistency require more careful design.
Concurrent Writes
Suppose two threads write to the same file.
Without coordination:
Thread A writes
Thread B writes
results can be surprising depending on operation and timing.
Potential issues:
- Lost updates
- Interleaved content
- Last writer wins
- Partial business state
File APIs do not automatically solve application-level concurrency.
Read-Modify-Write Risk
Weak pattern:
String current =
Files.readString(
path
);
String updated =
current
+ "\nNew content";
Files.writeString(
path,
updated
);
Two writers can both read the same old state and overwrite each other.
This is a classic lost-update problem.
Simple Overwrite vs Safer Replacement
Direct overwrite:
Files.writeString(
target,
content
);
is simple.
But if writing fails midway, depending on the system and failure mode, the target may not contain the intended complete content.
For important files, a safer pattern is:
Write temporary file
↓
Move temporary file over target
Temporary File Replacement Strategy
Concept:
course.md
Update safely by:
- Write new content to temporary file
- Ensure write completes
- Move temp file over target
Example:
Path temp =
Files.createTempFile(
target.getParent(),
"course-",
".tmp"
);
Then:
Files.writeString(
temp,
content,
StandardCharsets.UTF_8
);
Then:
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING
);
StandardCopyOption.REPLACE_EXISTING
Import:
import java.nio.file.StandardCopyOption;
Use:
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING
);
This replaces the target if it already exists.
Atomic Move
Java also provides:
StandardCopyOption.ATOMIC_MOVE
Example:
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
);
An atomic move means observers should see either:
Old file
or:
New file
rather than an intermediate state.
Atomic Move Is Not Always Supported
Important:
ATOMIC_MOVE
may not be supported by every file system or across different file systems.
It can throw:
AtomicMoveNotSupportedException
Therefore robust code may attempt atomic move and fall back when appropriate.
Safer Replace Helper
public static void replaceText(
Path target,
String content
) {
if (target == null) {
throw new IllegalArgumentException(
"Target path is required."
);
}
if (content == null) {
throw new IllegalArgumentException(
"Content is required."
);
}
Path parent =
target.toAbsolutePath()
.normalize()
.getParent();
if (parent == null) {
throw new IllegalArgumentException(
"Target parent directory is required."
);
}
Path temp = null;
try {
Files.createDirectories(
parent
);
temp =
Files.createTempFile(
parent,
"write-",
".tmp"
);
Files.writeString(
temp,
content,
StandardCharsets.UTF_8
);
try {
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
);
} catch (
AtomicMoveNotSupportedException exception
) {
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING
);
}
} catch (
IOException exception
) {
throw new IllegalStateException(
"Could not safely replace text file.",
exception
);
} finally {
if (
temp != null
&& Files.exists(
temp
)
) {
try {
Files.deleteIfExists(
temp
);
} catch (
IOException ignored
) {
// Cleanup failure should normally be logged.
}
}
}
}
Why Write the Temp File in the Same Directory?
Atomic rename behavior is strongest when source and target are on the same file system.
Creating the temp file inside:
target parent directory
increases the chance that the move can be atomic.
Cleanup Failure
If temporary-file cleanup fails:
Files.deleteIfExists(
temp
);
what should happen?
Usually the primary write failure remains more important.
The cleanup failure may be logged.
Avoid replacing the primary exception with an unrelated cleanup exception.
Do Not Silently Ignore Cleanup in Production
The previous example used:
catch (
IOException ignored
) {
}
only to keep the example compact.
In real application code, log useful context:
Temporary file could not be deleted
without hiding the primary operation result.
Write Failure Categories
File writing may fail because:
Parent directory missing
Permission denied
Disk full
Read-only file system
File already exists
Target is a directory
Storage unavailable
Do not translate every write failure to:
File already exists
Catch specific exceptions only when the distinction matters.
AccessDeniedException
Example:
catch (
AccessDeniedException exception
) {
throw new CourseContentStorageException(
"Course content cannot be written because storage access was denied.",
exception
);
}
This can provide useful internal classification.
External client still may receive a generic storage failure.
Storage Exception
public final class CourseContentStorageException
extends RuntimeException {
public CourseContentStorageException(
String message,
Throwable cause
) {
super(
message,
cause
);
}
}
Course Content Writer
package io.liveklass.content;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public final class CourseContentWriter {
private final Path baseDirectory;
public CourseContentWriter(
Path baseDirectory
) {
if (baseDirectory == null) {
throw new IllegalArgumentException(
"Base directory is required."
);
}
this.baseDirectory =
baseDirectory
.toAbsolutePath()
.normalize();
}
public void write(
String courseCode,
String fileName,
String content
) {
Path target =
resolveTarget(
courseCode,
fileName
);
try {
Path parent =
target.getParent();
if (parent != null) {
Files.createDirectories(
parent
);
}
Files.writeString(
target,
content,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE
);
} catch (
IOException exception
) {
throw new CourseContentStorageException(
"Could not write course content.",
exception
);
}
}
private Path resolveTarget(
String courseCode,
String fileName
) {
if (
courseCode == null
|| courseCode.isBlank()
) {
throw new IllegalArgumentException(
"Course code is required."
);
}
if (
fileName == null
|| fileName.isBlank()
) {
throw new IllegalArgumentException(
"File name is required."
);
}
Path target =
baseDirectory
.resolve(
courseCode
.strip()
.toLowerCase()
)
.resolve(
fileName
)
.normalize();
if (
!target.startsWith(
baseDirectory
)
) {
throw new IllegalArgumentException(
"Content path is outside the allowed directory."
);
}
return target;
}
}
Design Review
Why Create Parent Directories?
A valid content path may reference nested storage that does not exist yet.
Why Use Explicit Open Options?
This makes the intended behavior clear:
Create if missing
Overwrite if present
Write mode
Why Validate Final Path?
To prevent path traversal outside the allowed base directory.
Why Translate IOException?
Higher layers should depend on application storage semantics, not raw Java file-system details.
Create vs Update Contracts
Do not automatically use one method for every operation.
Clear APIs may separate:
create(...)
update(...)
append(...)
Why?
Because semantics differ.
Create Method
A strict create method can use:
StandardOpenOption.CREATE_NEW
Example:
public void create(
Path path,
String content
) throws IOException {
Files.writeString(
path,
content,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE
);
}
If file exists, creation fails.
Update Method
A strict update method might first require an existing regular file:
if (
!Files.isRegularFile(
path
)
) {
throw new IllegalStateException(
"Content file does not exist."
);
}
Then overwrite.
But remember:
Check does not guarantee later write.
The write itself still needs exception handling.
Upsert Method
Upsert means:
Create if missing
Update if present
Typical options:
CREATE
TRUNCATE_EXISTING
WRITE
This is different from strict create or strict update.
Name your method according to its actual contract.
Append Method
Append means:
Preserve existing content
Add new content at end
Typical:
CREATE
APPEND
Avoid Ambiguous save()
A method named:
save()
may mean:
Create?
Update?
Upsert?
Append?
Sometimes save is accepted by the application architecture.
But where behavior matters, more precise names improve clarity.
Writing Binary Data Is Different
This lesson focuses on text.
Text:
Files.writeString(...)
Binary:
Files.write(
path,
byte[]
)
or:
OutputStream
Examples of binary files:
Images
PDFs
ZIP archives
Video
Audio
Do not decode arbitrary binary files as UTF-8 text.
Large Text Output
writeString() accepts one whole String.
If you are generating huge output incrementally, building the entire content first may consume too much memory.
Example bad approach:
String gigantic =
buildHugeExport();
Files.writeString(
path,
gigantic
);
For large generated output, streaming writers are better.
We will cover streams and buffers in the next lesson.
Writing Lines from a List
List<String> topics =
List.of(
"Java",
"Spring Boot",
"PostgreSQL",
"Kafka"
);
Files.write(
path,
topics,
StandardCharsets.UTF_8
);
This is simple when data already exists as lines.
Files.write() Still Is Not Streaming from Your Source
If you already built:
List<String>
all data is already in memory.
For very large generated datasets, a BufferedWriter may be more appropriate.
Do Not Mix Validation and Storage Responsibilities Too Much
Storage component should know:
How to write a file
Where to store it
How to translate I/O failures
Domain service should know:
Whether content may be edited
Who can edit it
Whether course status allows updates
Example:
course.ensureEditable();
contentWriter.write(
courseCode,
fileName,
content
);
Avoid making file adapter decide course business rules.
Common Mistakes
Overwriting Existing Content Accidentally
Default write behavior can replace content.
Forgetting Parent Directories
Nested target paths may fail if parents do not exist.
Using APPEND Without a Line Separator
Content may join onto the previous line.
Using CREATE_NEW When Upsert Is Intended
Existing files will fail unexpectedly.
Using CREATE When Duplicate Creation Must Fail
May overwrite or reopen an existing file depending on options.
Validating After Writing
Invalid data may already be persisted.
Treating Pre-Checks as Guarantees
File-system state can change after the check.
Reading Then Rewriting for Simple Append
Creates unnecessary work and lost-update risk.
Ignoring Concurrent Writers
File operations alone do not solve application concurrency.
Losing the Original IOException
Makes diagnosis harder.
Writing Important Files Directly Without Considering Partial Failure
Temporary-file replacement may be safer for critical updates.
Assuming ATOMIC_MOVE Always Works
File system support varies.
Writing Huge Generated Content as One String
Can consume excessive memory.
Practice Exercises
Exercise 1: Write Markdown
Create:
void writeMarkdown(
Path path,
String content
)
Requirements:
- UTF-8
- Create parent directories
- Create or overwrite file
- Preserve
IOExceptionas cause
Exercise 2: Create Only Once
Create:
void createContent(
Path path,
String content
)
Use:
CREATE_NEW
Return a meaningful custom failure if file already exists.
Exercise 3: Append Activity
Append:
COURSE_PUBLISHED
to:
events.log
Requirements:
- Create file if missing
- Preserve old content
- Add a line separator
Exercise 4: Write Lines
Given:
List<String> topics
write them to:
topics.txt
using:
Files.write()
Exercise 5: Parent Directory
Given:
content/java/module-1/lesson.md
ensure all missing parent directories exist before writing.
Exercise 6: Safer Replace
Implement:
void replaceSafely(
Path target,
String content
)
using:
Temporary file
Files.writeString()
Files.move()
REPLACE_EXISTING
Try ATOMIC_MOVE first.
Exercise 7: Choose the Open Option
Choose the right strategy:
- Fail if a file already exists
- Add a new log line
- Replace existing Markdown content
- Create or replace course content
- Preserve existing file and add more content
Use:
CREATE
CREATE_NEW
APPEND
TRUNCATE_EXISTING
WRITE
Predict the Result
Question 1
Existing file:
Java
Then:
Files.writeString(
path,
"Spring"
);
What is the likely final content?
Answer
Spring
The old content is replaced.
Question 2
Existing file:
Java
Then:
Files.writeString(
path,
"Spring",
StandardCharsets.UTF_8,
StandardOpenOption.APPEND
);
What is the content?
Answer
JavaSpring
unless the original or appended content contains a line separator.
Question 3
A file already exists and you use:
CREATE_NEW
What happens?
Answer
Java throws:
FileAlreadyExistsException
Question 4
Does:
Files.createDirectories(
directory
)
fail simply because the directory already exists?
Answer
Normally no.
It ensures the directory path exists.
Question 5
Does:
ATOMIC_MOVE
work on every file system?
Answer
No.
It may throw:
AtomicMoveNotSupportedException
Knowledge Check
Question 1
What does Files.writeString() do?
Question 2
What usually happens when the target file already exists?
Question 3
What does CREATE_NEW mean?
Question 4
What does APPEND mean?
Question 5
What does TRUNCATE_EXISTING do?
Question 6
Why explicitly use UTF-8?
Question 7
Why create parent directories first?
Question 8
What is the difference between createDirectory() and createDirectories()?
Question 9
Why should validation happen before writing?
Question 10
Why is read-modify-write vulnerable to lost updates?
Question 11
What is a temporary-file replacement strategy?
Question 12
What does REPLACE_EXISTING do during move?
Question 13
Why is ATOMIC_MOVE useful?
Question 14
Why can it not always be relied upon?
Question 15
Why might BufferedWriter be preferable for very large generated output?
Knowledge Check Answers
Answer 1
It writes a String to a text file.
Answer 2
Existing content is normally replaced unless append or another explicit option is used.
Answer 3
Create the file only if it does not already exist.
Answer 4
Add new data to the end of existing content.
Answer 5
It clears existing content before writing new content.
Answer 6
To make character encoding predictable and consistent across systems.
Answer 7
Writing a file does not automatically guarantee all missing parent directories will be created.
Answer 8
createDirectory() creates one directory; createDirectories() creates missing parent levels too.
Answer 9
To avoid persisting invalid data before discovering the validation failure.
Answer 10
Multiple writers can read the same old state and later overwrite one another's changes.
Answer 11
Write new content to a temporary file, then move it over the target after the write succeeds.
Answer 12
It allows the destination file to be replaced if it already exists.
Answer 13
It can make replacement appear as one indivisible move rather than exposing an intermediate target state.
Answer 14
Atomic moves depend on file-system and storage capabilities.
Answer 15
It can write content incrementally instead of requiring one huge String in memory.
Lesson Summary
এই lesson-এ আমরা শিখেছি:
Files.writeString()text file লিখতে ব্যবহার করা যায়- UTF-8 explicitly define করা ভালো practice
- Default write behavior existing content replace করতে পারে
CREATE_NEWduplicate creation prevent করেAPPENDexisting content-এর শেষে data যোগ করে- Append automatically newline add করে না
TRUNCATE_EXISTINGold content clear করেFiles.write()line collections লিখতে useful- Nested files লেখার আগে parent directories create করতে হতে পারে
createDirectories()missing parent levels তৈরি করে- Validation side effect-এর আগে complete করা উচিত
- File-system pre-check later write success guarantee করে না
- Read-modify-write concurrent lost updates তৈরি করতে পারে
- File system application-level concurrency automatically solve করে না
- Critical updates temporary file + move strategy দিয়ে safer করা যায়
REPLACE_EXISTINGtarget replace করতে পারেATOMIC_MOVEstronger replacement semantics দিতে পারে- Atomic move support platform-dependent
- Original
IOExceptioncause preserve করা উচিত - Storage adapter file-system details higher layers থেকে hide করতে পারে
- Create, update, append, and upsert different contracts
- Very large generated text incremental writer দিয়ে handle করা ভালো
Next Lesson
পরবর্তী lesson:
Working with Streams and Buffers
আমরা শিখব:
- Byte streams vs character streams
InputStreamOutputStreamReaderWriterBufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter- Buffering কেন useful
- Binary vs text data
- Copying large files
- Streaming large output