File Handling and I/O
Reading Text Files
আপনি একটি free preview lesson দেখছেন।
Lesson Overview
Java application-এ text file পড়া একটি common task।
Examples:
Course Markdown content load করা
Configuration file পড়া
Import file process করা
Generated logs inspect করা
CSV data read করা
Template load করা
Java একাধিক API দেয়:
Files.readString()
Files.readAllLines()
Files.lines()
BufferedReader
সব API একই কাজের জন্য equally suitable নয়।
Main decision usually depends on:
File কত বড়?
Whole content একসঙ্গে দরকার?
Line-by-line process করতে হবে?
Memory usage গুরুত্বপূর্ণ?
Character encoding কী?
এই lesson-এ আমরা শিখব:
Files.readString()Files.readAllLines()Files.lines()BufferedReader- UTF-8 এবং character encoding
- Small vs large files
- Streaming file content
- Try-with-resources
- Empty file behavior
- Missing file handling
- Exception translation
- Safe file-reading design
Learning Objectives
এই lesson শেষে আপনি পারবেন:
- ছোট text file পুরোটা read করতে
- File lines হিসেবে read করতে
- Large file lazily process করতে
BufferedReaderব্যবহার করতে- Character encoding explicitly control করতে
IOExceptionhandle বা translate করতে- Memory-conscious reading strategy choose করতে
- File-storage details domain layer থেকে isolate করতে
Text Files Are Bytes on Disk
A file stored on disk ultimately contains bytes।
Example text:
Java
disk-এ character হিসেবে নয়, encoded bytes হিসেবে stored থাকে।
To convert bytes into characters, Java needs a:
Charset
Common charset:
UTF-8
Why Character Encoding Matters
Suppose file contains:
বাংলা
Java
é
€
If writer uses one encoding and reader assumes another, result can become corrupted।
This is called:
Mojibake
or garbled text।
Modern applications should generally standardize on:
UTF-8
unless another format is explicitly required।
StandardCharsets.UTF_8
Import:
import java.nio.charset.StandardCharsets;
Use:
StandardCharsets.UTF_8
This is safer than:
Charset.forName(
"UTF-8"
)
because the constant is predefined and typo-free।
Files.readString()
For a small or moderate text file where the entire content is needed:
String content =
Files.readString(
path
);
This returns the complete file as one String।
Basic Example
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(
String[] args
) throws IOException {
Path path =
Path.of(
"content",
"lesson.md"
);
String content =
Files.readString(
path
);
System.out.println(
content
);
}
}
Explicit UTF-8
You can specify encoding:
String content =
Files.readString(
path,
StandardCharsets.UTF_8
);
For application code, explicit encoding makes the file contract clearer।
What Does readString() Do?
Conceptually:
Open file
↓
Read all bytes
↓
Decode bytes into characters
↓
Create one String
↓
Close file
↓
Return String
You do not manually close anything।
When readString() Is a Good Choice
Good for:
- Markdown lesson files
- Small JSON files
- Templates
- Small configuration files
- Short text documents
Example:
String markdown =
Files.readString(
lessonPath,
StandardCharsets.UTF_8
);
Memory Cost of readString()
readString() loads the entire content into memory।
If file is:
5 KB
50 KB
2 MB
this is usually fine।
If file is:
5 GB
loading all content into one String is a bad idea।
Use streaming for large files।
Files.readAllLines()
If you need the entire file as individual lines:
List<String> lines =
Files.readAllLines(
path,
StandardCharsets.UTF_8
);
Example file:
Java
Spring
Kafka
Result:
[
"Java",
"Spring",
"Kafka"
]
Basic readAllLines() Example
Path path =
Path.of(
"topics.txt"
);
List<String> lines =
Files.readAllLines(
path,
StandardCharsets.UTF_8
);
for (
String line
: lines
) {
System.out.println(
line
);
}
readAllLines() Also Loads Everything
Important:
Files.readAllLines(...)
loads all lines into memory।
Difference from readString():
readString() → One String
readAllLines() → List<String>
Both are whole-file operations।
When readAllLines() Is Useful
Use it when:
- File is reasonably small
- Line boundaries matter
- You need random access by line
- You want to reuse the lines several times
Example:
String firstLine =
lines.get(
0
);
Empty File Behavior
For an empty file:
Files.readString(...)
returns:
""
An empty string।
For:
Files.readAllLines(...)
result is:
List.of()
conceptually an empty list।
A File with Blank Lines
File:
Java
Spring
readAllLines() preserves the blank line as an empty string:
"Java"
""
"Spring"
This can matter when reading Markdown or structured text।
Files.lines()
For streaming lines:
Stream<String> lines =
Files.lines(
path,
StandardCharsets.UTF_8
);
Unlike:
readAllLines()
it does not eagerly load all lines into a list।
Why Files.lines() Is Useful
Suppose file contains 1,000,000 lines।
You only need lines containing:
ERROR
Instead of loading every line into memory:
try (
Stream<String> lines =
Files.lines(
path,
StandardCharsets.UTF_8
)
) {
lines.filter(
line ->
line.contains(
"ERROR"
)
).forEach(
System.out::println
);
}
Files.lines() Must Be Closed
The returned stream uses an open file resource।
Therefore:
try (
Stream<String> lines =
Files.lines(
path
)
) {
}
is the correct pattern।
Weak Files.lines() Usage
Stream<String> lines =
Files.lines(
path
);
lines.forEach(
System.out::println
);
The stream may remain open longer than intended।
Better:
try (
Stream<String> lines =
Files.lines(
path
)
) {
lines.forEach(
System.out::println
);
}
Streams Are Lazy
With:
Files.lines(...)
the file is processed as the stream is consumed।
Example:
try (
Stream<String> lines =
Files.lines(
path
)
) {
lines.limit(
10
).forEach(
System.out::println
);
}
Only the needed portion may be processed rather than building a list of all lines first।
Count Lines
try (
Stream<String> lines =
Files.lines(
path,
StandardCharsets.UTF_8
)
) {
long count =
lines.count();
System.out.println(
count
);
}
Find the First Matching Line
try (
Stream<String> lines =
Files.lines(
path,
StandardCharsets.UTF_8
)
) {
String firstMatch =
lines.filter(
line ->
line.contains(
"Spring"
)
)
.findFirst()
.orElse(
null
);
System.out.println(
firstMatch
);
}
A Stream Cannot Be Reused
Weak:
Stream<String> lines =
Files.lines(
path
);
long count =
lines.count();
lines.forEach(
System.out::println
);
After:
count()
the stream is consumed।
Reuse throws:
IllegalStateException
If you need multiple passes:
- Read into a list
- Or open a new stream
BufferedReader
Another important way to read text:
BufferedReader
It supports efficient character-based sequential reading।
Import:
import java.io.BufferedReader;
Create with:
Files.newBufferedReader(...)
Basic BufferedReader
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
String line =
reader.readLine();
System.out.println(
line
);
}
readLine()
reader.readLine()
returns:
Next line as String
or:
null
when end of file is reached।
Read File Line by Line
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
String line;
while (
(
line =
reader.readLine()
)
!= null
) {
System.out.println(
line
);
}
}
This is a classic streaming pattern।
Why Use BufferedReader?
Useful when:
- Large file
- Sequential processing
- Fine control over reading
- Parsing records manually
- Need to stop early
- Need custom state while reading
Example:
Read until a marker
Track line number
Parse blocks
Skip headers
Track Line Numbers
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
String line;
int lineNumber = 0;
while (
(
line =
reader.readLine()
)
!= null
) {
lineNumber++;
System.out.println(
lineNumber
+ ": "
+ line
);
}
}
Stop Reading Early
while (
(
line =
reader.readLine()
)
!= null
) {
if (
line.equals(
"---END---"
)
) {
break;
}
process(
line
);
}
Only required content is processed।
Files.lines() vs BufferedReader
Both can stream text।
Files.lines()
Strong when:
- Stream operations fit naturally
- Filtering
- Mapping
- Counting
- Simple line transformations
Example:
lines.filter(...)
.map(...)
.count();
BufferedReader
Strong when:
- Stateful parsing
- Need line number
- Need custom loop control
- Multi-line record parsing
- More imperative control
readString() vs readAllLines() vs Files.lines()
A simple guide:
| API | Loads whole file? | Best for |
|---|---|---|
readString() | Yes | Entire text content |
readAllLines() | Yes | Small file as lines |
Files.lines() | No, streamed | Stream processing |
BufferedReader | No, streamed | Controlled sequential parsing |
Example: Reading Markdown Content
A lesson content service may simply need:
public String loadLessonContent(
Path path
) throws IOException {
return Files.readString(
path,
StandardCharsets.UTF_8
);
}
For a typical lesson Markdown file, this is clear and practical।
Do not use a complex stream just because streaming exists।
Use the Simplest Correct API
Weak overengineering:
StringBuilder content =
new StringBuilder();
try (
BufferedReader reader =
Files.newBufferedReader(
path
)
) {
String line;
while (
(
line =
reader.readLine()
)
!= null
) {
content.append(
line
);
content.append(
System.lineSeparator()
);
}
}
if all you need is:
String content =
Files.readString(
path
);
Choose based on requirements, not API complexity।
Line Separators
Different systems historically use different line endings:
Linux/macOS → \n
Windows → \r\n
Text APIs such as:
BufferedReader.readLine()
handle common line separators for you।
readLine() returns the line content without the line terminator।
Why This Matters
Suppose file:
Java\nSpring\n
readLine() returns:
Java
Spring
not strings ending with:
\n
If exact original formatting matters, readString() may be more appropriate।
Character Encoding Example
Suppose:
lesson-bn.md
contains Bangla।
Read explicitly as UTF-8:
String content =
Files.readString(
path,
StandardCharsets.UTF_8
);
This keeps the storage contract obvious।
Missing File
If path does not exist:
Files.readString(
path
)
can throw:
NoSuchFileException
which is an IOException subtype।
Handling Missing File Specifically
try {
return Files.readString(
path,
StandardCharsets.UTF_8
);
} catch (
NoSuchFileException exception
) {
throw new CourseContentNotFoundException(
"Course content file was not found.",
exception
);
} catch (
IOException exception
) {
throw new CourseContentStorageException(
"Could not read course content.",
exception
);
}
Specific failure is handled before broader:
IOException
Do Not Treat Every IOException as Missing File
Weak:
catch (
IOException exception
) {
return null;
}
An IOException might mean:
- File missing
- Permission denied
- Disk failure
- Broken file system
- I/O interruption
These are not all:
Content not found
File Existence Pre-Check Is Not Enough
Weak:
if (
Files.exists(
path
)
) {
return Files.readString(
path
);
}
Between check and read:
- File can be deleted
- Permission can change
- Storage can fail
The read operation must still handle exceptions।
Avoid Check-Then-Read When the Exception Is Enough
Often this:
try {
return Files.readString(
path
);
} catch (
NoSuchFileException exception
) {
// Not found
}
is enough।
A separate:
Files.exists(...)
may create unnecessary duplicate file-system operations।
Exception Translation
Low-level method:
Files.readString(...)
throws:
IOException
Higher application code may prefer:
CourseContentStorageException
This keeps storage implementation details inside the adapter layer।
Custom Storage Exception
public final class CourseContentStorageException
extends RuntimeException {
public CourseContentStorageException(
String message,
Throwable cause
) {
super(
message,
cause
);
}
}
Course Content Reader
package io.liveklass.content;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public final class CourseContentReader {
private final Path baseDirectory;
public CourseContentReader(
Path baseDirectory
) {
if (baseDirectory == null) {
throw new IllegalArgumentException(
"Base directory is required."
);
}
this.baseDirectory =
baseDirectory
.toAbsolutePath()
.normalize();
}
public String read(
String courseCode,
String fileName
) {
Path path =
buildPath(
courseCode,
fileName
);
try {
return Files.readString(
path,
StandardCharsets.UTF_8
);
} catch (
IOException exception
) {
throw new CourseContentStorageException(
"Could not read course content.",
exception
);
}
}
private Path buildPath(
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 courseDirectory =
baseDirectory.resolve(
courseCode
.strip()
.toLowerCase()
);
Path target =
courseDirectory.resolve(
fileName
)
.normalize();
if (
!target.startsWith(
baseDirectory
)
) {
throw new IllegalArgumentException(
"Content path is outside the allowed directory."
);
}
return target;
}
}
Design Review
Why Keep baseDirectory Normalized?
Every resolved target is compared against one stable base।
Why Validate the Final Target?
External path components may contain:
..
which could escape the allowed directory।
Why Translate IOException?
Higher application layers should understand:
Course content storage failure
instead of depending directly on Java file API details।
Reading Only the First Line
Simple:
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
return reader.readLine();
}
If file is empty:
readLine()
returns:
null
First Non-Blank Line
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
String line;
while (
(
line =
reader.readLine()
)
!= null
) {
if (!line.isBlank()) {
return line;
}
}
return null;
}
Count Non-Blank Lines with Files.lines()
try (
Stream<String> lines =
Files.lines(
path,
StandardCharsets.UTF_8
)
) {
return lines.filter(
line ->
!line.isBlank()
).count();
}
Search Content Without Loading Everything
public static boolean containsText(
Path path,
String searchText
) throws IOException {
try (
Stream<String> lines =
Files.lines(
path,
StandardCharsets.UTF_8
)
) {
return lines.anyMatch(
line ->
line.contains(
searchText
)
);
}
}
anyMatch() can stop early after finding the first match।
Large File Processing
Imagine a 10 GB log file।
Bad:
String content =
Files.readString(
path
);
Better:
try (
Stream<String> lines =
Files.lines(
path
)
) {
lines.filter(...)
.forEach(...);
}
or:
BufferedReader
Memory stays bounded relative to whole-file loading।
Streaming Does Not Mean Zero Memory
Streaming still uses:
- Buffers
- Current line objects
- Processing state
But it avoids keeping the entire file content in memory at once।
Be Careful Collecting a Stream
This starts streamed:
Files.lines(...)
but:
lines.toList()
loads all resulting lines into memory।
So:
Files.lines(path).toList()
may have similar memory characteristics to:
Files.readAllLines(path)
for large files।
Choose Based on Final Operation
If you immediately need:
List<String>
for a small file:
readAllLines()
is simpler।
If you need only:
Count matching lines
Find first match
Transform and write elsewhere
streaming is better।
Closing Happens Even on Exception
try (
BufferedReader reader =
Files.newBufferedReader(
path
)
) {
process(
reader
);
}
If:
process(...)
throws, reader is still closed automatically।
This is why try-with-resources is preferred।
Do Not Manually Close Inside Try-With-Resources
Unnecessary:
try (
BufferedReader reader =
Files.newBufferedReader(
path
)
) {
process(
reader
);
reader.close();
}
The resource is automatically closed।
Manual close adds no benefit and may complicate logic।
Do Not Return the Reader from a Closed Scope
Weak:
public BufferedReader open(
Path path
) throws IOException {
try (
BufferedReader reader =
Files.newBufferedReader(
path
)
) {
return reader;
}
}
The returned reader is already closed।
Either:
- Return the content
- Let caller own the reader
- Provide a callback-based processing API
Ownership must be clear।
Resource Ownership
If a method opens a resource and fully processes it, that method should usually close it।
Example:
public String read(
Path path
)
Method owns the file resource।
If a method returns:
BufferedReader
caller becomes responsible for closing it।
APIs should make ownership obvious।
Common Mistakes
Using readString() for Huge Files
Loads the entire file into memory।
Using readAllLines() for Huge Files
Also loads everything into memory।
Forgetting to Close Files.lines()
Leaks file resources।
Reusing a Consumed Stream
Streams are single-use।
Assuming UTF-8 Without Defining the Contract
Can create encoding bugs when systems differ।
Returning Empty String for Every Read Failure
Hides the difference between empty content and failed I/O।
Treating All IOExceptions as Not Found
Different I/O failures have different meanings।
Doing exists() Then Assuming Read Will Succeed
File-system state can change between operations।
Returning a Reader from Try-With-Resources
The reader is already closed।
Collecting a Huge Stream into a List
Removes the memory advantage of streaming।
Overengineering Small File Reads
Use readString() when that is the simplest correct solution।
Practice Exercises
Exercise 1: Read Markdown
Create:
String readMarkdown(
Path path
)
Requirements:
- UTF-8
Files.readString()- Wrap
IOException - Preserve the cause
Exercise 2: Read Topics
Given:
Java
Spring
Kafka
Redis
Use:
Files.readAllLines()
and print each topic with an index।
Exercise 3: Count Non-Blank Lines
Use:
Files.lines()
to count non-blank lines without collecting them into a list।
Exercise 4: Search a Large File
Create:
boolean containsText(
Path path,
String text
)
Use streaming and stop after the first match।
Exercise 5: Buffered Reader
Read a file line-by-line and print:
1: first line
2: second line
Use try-with-resources।
Exercise 6: Missing File
Handle:
NoSuchFileException
separately from other:
IOException
and translate them into different application-level failures।
Exercise 7: Choose the API
Choose the best starting API:
- Load a 20 KB Markdown lesson as one string
- Read a 50-line configuration file into a list
- Search a 5 GB log file for the first matching line
- Parse a large multiline record format with custom state
Choose among:
readString()
readAllLines()
Files.lines()
BufferedReader
Predict the Result
Question 1
String content =
Files.readString(
emptyFile
);
System.out.println(
content.length()
);
For an empty file, what prints?
Answer
0
Question 2
List<String> lines =
Files.readAllLines(
emptyFile
);
System.out.println(
lines.size()
);
Answer
0
Question 3
BufferedReader reader =
...;
String line =
reader.readLine();
What does readLine() return at end of file?
Answer
null
Question 4
Can a Stream<String> returned by Files.lines() be consumed twice?
Answer
No।
Streams are single-use।
Question 5
Does:
Files.lines(path).toList()
preserve streaming memory benefits for a huge file?
Answer
Not fully।
The resulting list stores all collected lines in memory।
Knowledge Check
Question 1
What does Files.readString() return?
Question 2
When is readString() appropriate?
Question 3
What does readAllLines() return?
Question 4
Does readAllLines() load the complete file?
Question 5
Why is Files.lines() useful?
Question 6
Why must its stream be closed?
Question 7
What does BufferedReader.readLine() return at EOF?
Question 8
When is BufferedReader preferable?
Question 9
Why specify UTF-8 explicitly?
Question 10
What is the risk of reading a huge file with readString()?
Question 11
Why should every IOException not become not-found?
Question 12
Why can Files.exists() not guarantee a later read?
Question 13
What is exception translation in file storage code?
Question 14
Who should close a resource opened inside a method?
Question 15
Why should a Stream not be returned after being closed?
Knowledge Check Answers
Answer 1
The complete text file content as one String।
Answer 2
When the file is reasonably small and the whole content is needed।
Answer 3
A List<String> containing the file lines।
Answer 4
Yes।
Answer 5
It supports lazy line-by-line processing without eagerly loading the entire file।
Answer 6
The stream holds an underlying open file resource।
Answer 7
null।
Answer 8
When custom sequential parsing, line numbers, early stopping, or stateful processing is needed।
Answer 9
It makes the text-storage contract explicit and prevents charset mismatches।
Answer 10
The entire content must fit in memory and can cause excessive memory usage or failure।
Answer 11
IOException also represents permission, disk, and other I/O failures।
Answer 12
The file system can change between the check and actual operation।
Answer 13
Converting low-level IOException into an application-specific storage failure while preserving the cause।
Answer 14
Usually the method that opens and fully uses the resource should close it।
Answer 15
The caller would receive an unusable resource।
Lesson Summary
এই lesson-এ আমরা শিখেছি:
- Text files are stored as encoded bytes
- UTF-8 is a common application encoding
StandardCharsets.UTF_8makes encoding explicitFiles.readString()returns the entire file as oneStringreadString()is suitable for reasonably small filesFiles.readAllLines()returns all lines as aList<String>- Both
readString()andreadAllLines()load the full file into memory Files.lines()supports streamed line processingFiles.lines()must be closed with try-with-resources- Streams are lazy and single-use
- Collecting a large stream into a list can remove its memory advantage
BufferedReadersupports controlled sequential readingreadLine()returnsnullat end of fileBufferedReaderis useful for stateful parsing and line tracking- Try-with-resources guarantees resource cleanup
- Missing file is only one possible
IOException - File existence pre-check does not guarantee later read success
- File adapters can translate low-level I/O exceptions into application-level exceptions
- Original exception causes should be preserved
- Resource ownership should be explicit
- The simplest correct reading API should be preferred
Next Lesson
পরবর্তী lesson:
Writing and Updating Text Files
আমরা শিখব:
Files.writeString()- Creating new files
- Overwriting files
- Appending content
StandardOpenOption- Writing lists of lines
- UTF-8 output
- Parent directory handling
- Safe update strategies
- Temporary-file replacement
- Write failure handling