File Handling and I/O
Working with Streams and Buffers
আপনি একটি free preview lesson দেখছেন।
Lesson Overview
আগের lessons-এ আমরা convenient high-level APIs ব্যবহার করেছি:
Files.readString()
Files.writeString()
Files.lines()
কিন্তু file handling-এর foundation আরও নিচে রয়েছে:
InputStream
OutputStream
Reader
Writer
এগুলো dataকে একবারে পুরোটা memory-তে load না করে ধাপে ধাপে process করতে পারে।
এই lesson-এ আমরা শিখব:
- Byte stream কী
- Character stream কী
InputStreamOutputStreamReaderWriterBufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter- Buffering কেন useful
- Binary এবং text data-এর পার্থক্য
- Large file copy করা
- Incrementally large text output লেখা
- Resource ownership
- Try-with-resources
Learning Objectives
এই lesson শেষে আপনি পারবেন:
- Byte-based এবং character-based I/O distinguish করতে
InputStreamএবংOutputStreamব্যবহার করতেReaderএবংWriterব্যবহার করতে- Buffering-এর purpose explain করতে
- Binary file safely copy করতে
- Large text incrementally লিখতে
- Charset conversion কোথায় প্রয়োজন বুঝতে
- Stream resources safely close করতে
What Is a Stream?
A stream হলো data-এর sequential flow।
Conceptually:
Source
↓
data
↓
data
↓
data
↓
Consumer
Input stream:
File → Application
Output stream:
Application → File
A stream সাধারণত পুরো file একসঙ্গে represent করে না।
It provides:
Read the next portion
Write the next portion
Why Streams Matter
Suppose a file is:
8 GB
Whole-file approach:
byte[] data =
Files.readAllBytes(
path
);
requires huge memory।
Stream-based approach reads:
Small chunk
Process
Small chunk
Process
...
Memory usage remains bounded।
Two Main I/O Families
Java I/O broadly has:
Byte-oriented I/O
Character-oriented I/O
Byte-oriented:
InputStream
OutputStream
Character-oriented:
Reader
Writer
Byte Streams
Byte streams work with raw bytes।
Use for:
- Images
- PDFs
- ZIP files
- Audio
- Video
- Protocol payloads
- Any arbitrary binary data
Core abstractions:
InputStream
OutputStream
Character Streams
Character streams work with text characters।
Use for:
- Markdown
- JSON
- CSV
- Source code
- Configuration
- Plain text
Core abstractions:
Reader
Writer
Character streams involve:
Byte ↔ Character conversion
using a charset such as UTF-8।
Never Treat Arbitrary Binary Data as Text
Weak:
String content =
Files.readString(
Path.of(
"image.png"
)
);
A PNG file is not UTF-8 text।
Correct approach:
InputStream
or:
Files.readAllBytes()
for small binary data।
InputStream
InputStream is the base abstraction for reading bytes।
Import:
import java.io.InputStream;
Open a file stream:
InputStream input =
Files.newInputStream(
path
);
Reading One Byte
int value =
input.read();
Why int instead of byte?
Because read() needs to represent:
0–255 → Actual byte
-1 → End of stream
Example:
int value;
while (
(
value =
input.read()
)
!= -1
) {
System.out.println(
value
);
}
Reading One Byte at a Time Is Usually Inefficient
This:
input.read()
for every byte creates many I/O calls।
For large data, use a byte buffer।
Reading into a Byte Array
byte[] buffer =
new byte[8192];
int bytesRead =
input.read(
buffer
);
Possible return:
Positive number → Number of bytes read
-1 → End of stream
Stream Read Loop
byte[] buffer =
new byte[8192];
int bytesRead;
while (
(
bytesRead =
input.read(
buffer
)
)
!= -1
) {
process(
buffer,
bytesRead
);
}
Important:
Only the first:
bytesRead
bytes are valid for the current iteration।
Why bytesRead Matters
Suppose buffer size:
8192
Last read returns:
123
Only:
buffer[0] ... buffer[122]
belong to the current read।
Do not process the entire 8192 bytes blindly।
OutputStream
OutputStream writes raw bytes।
Import:
import java.io.OutputStream;
Open:
OutputStream output =
Files.newOutputStream(
path
);
Write:
output.write(
data
);
where:
data
is a:
byte[]
Write Part of a Buffer
When copying from an input stream:
output.write(
buffer,
0,
bytesRead
);
This writes only the valid bytes received in the latest read।
Complete Binary File Copy
public static void copyFile(
Path source,
Path target
) {
try (
InputStream input =
Files.newInputStream(
source
);
OutputStream output =
Files.newOutputStream(
target
)
) {
byte[] buffer =
new byte[8192];
int bytesRead;
while (
(
bytesRead =
input.read(
buffer
)
)
!= -1
) {
output.write(
buffer,
0,
bytesRead
);
}
} catch (
IOException exception
) {
throw new IllegalStateException(
"Could not copy file.",
exception
);
}
}
This does not load the entire source into memory।
Buffer Size
Example:
new byte[8192]
means:
8 KB buffer
There is no universal perfect buffer size।
Common values include:
4 KB
8 KB
16 KB
64 KB
Performance depends on:
- Storage
- Operating system
- Workload
- Network
- File size
Do not prematurely optimize buffer size without measurement।
transferTo()
Modern Java also provides:
input.transferTo(
output
);
Example:
try (
InputStream input =
Files.newInputStream(
source
);
OutputStream output =
Files.newOutputStream(
target
)
) {
input.transferTo(
output
);
}
This is much simpler for direct stream copying।
Use manual buffer loops when you need custom processing or progress tracking।
BufferedInputStream
A BufferedInputStream adds buffering around another input stream।
BufferedInputStream input =
new BufferedInputStream(
Files.newInputStream(
path
)
);
Import:
import java.io.BufferedInputStream;
Why Buffering Helps
Without buffering, small reads may repeatedly reach the underlying file or device।
Buffered input conceptually does:
Read larger chunk from storage
↓
Keep it in memory buffer
↓
Serve smaller reads from the buffer
This can reduce expensive underlying I/O operations।
BufferedOutputStream
Similarly:
BufferedOutputStream output =
new BufferedOutputStream(
Files.newOutputStream(
path
)
);
Writes can accumulate in memory and be sent in larger chunks।
Flush
Buffered output may temporarily hold data before sending it to the underlying destination।
You can force buffered data to be written:
output.flush();
Closing the stream also flushes it as part of normal close behavior।
Do Not Flush After Every Tiny Write
Weak:
output.write(
value
);
output.flush();
inside a tight loop।
This defeats much of the benefit of buffering।
Flush when:
- Data must become visible immediately
- Protocol requires it
- Long-lived writer reaches a logical boundary
For ordinary file writing, close is often enough।
Buffered Binary Copy
try (
InputStream input =
new BufferedInputStream(
Files.newInputStream(
source
)
);
OutputStream output =
new BufferedOutputStream(
Files.newOutputStream(
target
)
)
) {
input.transferTo(
output
);
}
Simple and memory-efficient।
Reader
Reader is the base abstraction for reading characters।
Import:
import java.io.Reader;
A common file reader:
Reader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
);
BufferedReader is itself a Reader subtype।
Writer
Writer is the base abstraction for writing characters।
Import:
import java.io.Writer;
A common writer:
Writer writer =
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8
);
Charset Conversion
A file stores bytes।
Character reader conceptually performs:
Bytes
↓ UTF-8 decoding
Characters
Writer performs:
Characters
↓ UTF-8 encoding
Bytes
This is why charset matters for text streams।
BufferedReader
We used it previously:
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
String line;
while (
(
line =
reader.readLine()
)
!= null
) {
process(
line
);
}
}
It combines:
Character decoding
+
Buffering
+
Convenient line reading
BufferedWriter
For large text output:
BufferedWriter writer =
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8
);
Import:
import java.io.BufferedWriter;
Writing with BufferedWriter
try (
BufferedWriter writer =
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8
)
) {
writer.write(
"Java"
);
writer.newLine();
writer.write(
"Spring"
);
}
Output:
Java
Spring
newLine()
Instead of manually writing:
"\n"
you can use:
writer.newLine();
It writes the platform-specific line separator।
Large Generated Text Output
Suppose you need to generate one million lines।
Weak:
StringBuilder builder =
new StringBuilder();
for (
int i = 1;
i <= 1_000_000;
i++
) {
builder.append(
i
);
builder.append(
'\n'
);
}
Files.writeString(
path,
builder.toString()
);
The entire output exists in memory first।
Streamed Text Generation
Better:
try (
BufferedWriter writer =
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8
)
) {
for (
int i = 1;
i <= 1_000_000;
i++
) {
writer.write(
Integer.toString(
i
)
);
writer.newLine();
}
}
Only a small buffer is kept in memory।
Why This Scales Better
Whole-string approach:
Generate everything
↓
Keep everything in memory
↓
Write everything
Buffered writer:
Generate small amount
↓
Buffer
↓
Write
↓
Continue
This keeps memory usage bounded।
Appending with BufferedWriter
try (
BufferedWriter writer =
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
)
) {
writer.write(
"COURSE_PUBLISHED"
);
writer.newLine();
}
Reader vs InputStream
Use InputStream when the data is:
Bytes
Use Reader when the data is:
Text characters
Example:
PNG → InputStream
PDF → InputStream
Markdown → Reader
CSV → Reader
Writer vs OutputStream
Use OutputStream for:
Raw binary bytes
Use Writer for:
Text
Example:
Image upload → OutputStream
Markdown export → Writer
Converting Bytes to Characters Manually
Sometimes you already have an InputStream and need text decoding।
Use:
InputStreamReader
Example:
Reader reader =
new InputStreamReader(
inputStream,
StandardCharsets.UTF_8
);
Import:
import java.io.InputStreamReader;
Add Buffering Around InputStreamReader
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
inputStream,
StandardCharsets.UTF_8
)
);
Conceptual layers:
File/network bytes
↓
InputStream
↓
InputStreamReader
↓ UTF-8 decoding
BufferedReader
↓
Application strings
Characters to Bytes
Opposite direction uses:
OutputStreamWriter
Example:
Writer writer =
new OutputStreamWriter(
outputStream,
StandardCharsets.UTF_8
);
Then optionally:
BufferedWriter
around it।
Layered I/O Design
Java I/O often uses wrappers।
Example:
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
new BufferedInputStream(
Files.newInputStream(
path
)
),
StandardCharsets.UTF_8
)
);
This works, but for ordinary files:
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
is much simpler।
Use lower-level layering only when needed।
Do Not Add Wrappers Without a Reason
Weak overengineering:
FileInputStream
→ BufferedInputStream
→ InputStreamReader
→ BufferedReader
when:
Files.newBufferedReader(...)
already provides the desired text reader।
Understand the layers, but prefer concise APIs।
Stream Ownership
Suppose a method receives:
InputStream input
Should it close it?
Not automatically।
Ownership must be defined by the API contract।
Common convention:
Method opens stream → Method closes stream
Caller opens stream → Caller closes stream
Example: Method Opens the Resource
public void processFile(
Path path
) {
try (
InputStream input =
Files.newInputStream(
path
)
) {
process(
input
);
}
}
This method owns the stream।
Example: Method Receives the Resource
public void process(
InputStream input
) throws IOException {
// Read it
}
Usually this method should not close the stream unless the contract explicitly says it owns it।
Why?
The caller may want to use the stream afterward।
Returning an Open Stream
A method can return:
InputStream
but then the caller must close it।
Example:
public InputStream open(
Path path
) throws IOException {
return Files.newInputStream(
path
);
}
Caller:
try (
InputStream input =
storage.open(
path
)
) {
process(
input
);
}
Resource ownership must be obvious।
Do Not Return a Closed Stream
Wrong:
public InputStream open(
Path path
) throws IOException {
try (
InputStream input =
Files.newInputStream(
path
)
) {
return input;
}
}
The stream is closed before caller receives it।
close() and Try-With-Resources
Correct:
try (
InputStream input =
Files.newInputStream(
path
)
) {
// Use input
}
You usually do not need:
input.close();
manually।
Multiple Resources
try (
InputStream input =
Files.newInputStream(
source
);
OutputStream output =
Files.newOutputStream(
target
)
) {
input.transferTo(
output
);
}
Resources close in reverse declaration order:
output
then
input
Binary Copy with Progress
Manual buffer loop is useful when tracking progress।
public static long copyWithProgress(
Path source,
Path target
) {
try (
InputStream input =
new BufferedInputStream(
Files.newInputStream(
source
)
);
OutputStream output =
new BufferedOutputStream(
Files.newOutputStream(
target
)
)
) {
byte[] buffer =
new byte[8192];
long totalBytes =
0L;
int bytesRead;
while (
(
bytesRead =
input.read(
buffer
)
)
!= -1
) {
output.write(
buffer,
0,
bytesRead
);
totalBytes +=
bytesRead;
System.out.println(
"Copied "
+ totalBytes
+ " bytes"
);
}
return totalBytes;
} catch (
IOException exception
) {
throw new IllegalStateException(
"Could not copy file.",
exception
);
}
}
Large File Hashing Concept
Streams also allow operations that do not create another file।
Example:
Read chunk
↓
Update hash
↓
Discard chunk
↓
Read next chunk
You do not need the entire file in memory।
This pattern is useful for:
- Checksums
- Upload validation
- Compression
- Encryption
- Parsing
Buffering Does Not Make Every Operation Fast
Buffering reduces small underlying I/O operations, but total performance still depends on:
- Disk speed
- Network bandwidth
- CPU
- Encoding
- Compression
- Processing logic
Always measure real bottlenecks before complex optimization।
Buffering and Memory
A buffer uses memory intentionally to reduce I/O overhead।
Example:
8 KB buffer
is tiny compared with loading a multi-GB file।
The goal is:
Small bounded memory
instead of
Whole-file memory
Binary vs Text Copy
For binary:
InputStream
OutputStream
Do not decode/encode।
For text transformation:
Reader
Writer
Example:
Read UTF-8
Convert to uppercase
Write UTF-8
Text Transformation Example
public static void uppercaseFile(
Path source,
Path target
) {
try (
BufferedReader reader =
Files.newBufferedReader(
source,
StandardCharsets.UTF_8
);
BufferedWriter writer =
Files.newBufferedWriter(
target,
StandardCharsets.UTF_8
)
) {
String line;
while (
(
line =
reader.readLine()
)
!= null
) {
writer.write(
line.toUpperCase()
);
writer.newLine();
}
} catch (
IOException exception
) {
throw new IllegalStateException(
"Could not transform text file.",
exception
);
}
}
Exact Line Endings May Change
The previous transformation uses:
readLine()
and:
newLine()
Therefore original line separators are not preserved exactly।
If exact byte representation matters, use byte streams or a more precise transformation strategy।
Do Not Use Character Streams for Exact Binary Preservation
Suppose you copy a file with:
Reader
Writer
Potential changes:
- Character decoding
- Encoding
- Invalid byte replacement
- Line-ending differences depending on logic
For exact file copies, use byte streams or:
Files.copy()
High-Level API Still Matters
If your only goal is:
Copy file exactly
you may simply use:
Files.copy(
source,
target
);
You do not need to manually implement streams।
Learn low-level streams so you know how I/O works and can use them when custom processing is required।
Example: Course Export Writer
Suppose thousands of course titles must be exported.
public final class CourseExportWriter {
public void writeTitles(
Path path,
Iterable<String> titles
) {
if (path == null) {
throw new IllegalArgumentException(
"Export path is required."
);
}
if (titles == null) {
throw new IllegalArgumentException(
"Course titles are required."
);
}
try (
BufferedWriter writer =
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8
)
) {
for (
String title
: titles
) {
if (title == null) {
continue;
}
writer.write(
title
);
writer.newLine();
}
} catch (
IOException exception
) {
throw new CourseExportException(
"Could not write course export.",
exception
);
}
}
}
Storage Boundary Exception
public final class CourseExportException
extends RuntimeException {
public CourseExportException(
String message,
Throwable cause
) {
super(
message,
cause
);
}
}
Again:
IOException
is translated at the storage/application boundary।
Common Mistakes
Reading Large Binary Files into One Byte Array
May consume excessive memory।
Reading One Byte at a Time
Creates unnecessary I/O overhead।
Ignoring bytesRead
Can write stale bytes from the buffer।
Using Text Readers for Binary Data
May corrupt the data।
Using Byte Streams for Text Without Considering Charset
Raw bytes do not automatically become correct characters।
Forgetting to Close Streams
Can leak file descriptors and other resources।
Returning a Resource Closed by Try-With-Resources
Caller receives an unusable stream।
Closing a Caller-Owned Stream Unexpectedly
Can break caller logic।
Flushing After Every Tiny Write
Can destroy buffering benefits।
Building Huge Strings Before Writing
Consumes unnecessary memory।
Assuming Buffering Solves All Performance Problems
Other bottlenecks may dominate।
Reimplementing Files.copy() Without a Need
Prefer high-level APIs when they already satisfy the requirement।
Practice Exercises
Exercise 1: Copy an Image
Create:
void copyImage(
Path source,
Path target
)
Use:
InputStream
OutputStream
byte[]
Do not load the whole image into memory।
Exercise 2: Use transferTo()
Rewrite the file copy using:
InputStream.transferTo()
and try-with-resources।
Exercise 3: Buffered Binary Copy
Wrap both streams in:
BufferedInputStream
BufferedOutputStream
and explain what buffering provides।
Exercise 4: Generate a Large Text File
Write numbers:
1
2
3
...
1,000,000
using:
BufferedWriter
without building one giant string first।
Exercise 5: Text Transformation
Read a UTF-8 text file line-by-line and write only non-blank lines to another UTF-8 file।
Use:
BufferedReader
BufferedWriter
Exercise 6: Choose Byte or Character I/O
Choose the correct family:
- PNG image
- Markdown lesson
- ZIP archive
- CSV file
- JSON document
Choose:
InputStream/OutputStream
Reader/Writer
Exercise 7: Resource Ownership
A method receives:
InputStream input
from its caller।
Should it close the stream?
Explain why the method contract must define ownership।
Predict the Result
Question 1
int value =
input.read();
What does -1 mean?
Answer
End of stream
Question 2
byte[] buffer =
new byte[8192];
int bytesRead =
input.read(
buffer
);
If bytesRead is 100, how many bytes should be written?
Answer
Exactly:
100
using:
output.write(
buffer,
0,
bytesRead
);
Question 3
Can a PDF safely be copied through:
BufferedReader
BufferedWriter
as text?
Answer
No।
PDF is binary data and should be handled with byte-oriented I/O for exact copying।
Question 4
Does closing a BufferedWriter normally flush remaining buffered output?
Answer
Yes।
Normal close flushes remaining buffered content before closing the underlying writer।
Question 5
Is BufferedWriter useful when generating very large output incrementally?
Answer
Yes।
It avoids building the complete output as one large in-memory string।
Knowledge Check
Question 1
What is a stream?
Question 2
What is the difference between InputStream and OutputStream?
Question 3
When should byte streams be used?
Question 4
When should character streams be used?
Question 5
What does InputStream.read() return at EOF?
Question 6
Why read into a byte buffer?
Question 7
Why must bytesRead be respected?
Question 8
What does buffering provide?
Question 9
What does flush() do?
Question 10
What is the relationship between Reader and charset?
Question 11
When is BufferedReader useful?
Question 12
When is BufferedWriter useful?
Question 13
Why use try-with-resources?
Question 14
Who should usually close a stream?
Question 15
Why use a high-level API such as Files.copy() when possible?
Knowledge Check Answers
Answer 1
A sequential flow of data between a source and a consumer।
Answer 2
InputStream reads bytes into the application; OutputStream writes bytes out of the application।
Answer 3
For binary data or when exact raw bytes matter।
Answer 4
For text where bytes must be decoded or encoded using a charset।
Answer 5
-1
Answer 6
To process multiple bytes per operation and keep memory usage bounded।
Answer 7
Only that many bytes in the buffer belong to the latest read operation।
Answer 8
It reduces frequent small operations against the underlying I/O resource by processing data in larger chunks।
Answer 9
It forces currently buffered output toward the underlying destination।
Answer 10
A Reader converts encoded bytes into Java characters using a charset somewhere in the reader chain।
Answer 11
For efficient sequential text reading, line-based processing, and stateful parsing।
Answer 12
For efficient incremental text output, especially when generated content is large।
Answer 13
It closes resources automatically even when exceptions occur।
Answer 14
Usually the component that opens and owns the stream should close it।
Answer 15
It is simpler, less error-prone, and already implements the common operation correctly।
Lesson Summary
এই lesson-এ আমরা শিখেছি:
- Streams sequential data flow represent করে
InputStreamএবংOutputStreambyte-oriented I/OReaderএবংWritercharacter-oriented I/O- Binary files raw byte streams দিয়ে handle করা উচিত
- Text requires character encoding such as UTF-8
- Whole-file loading large files-এর জন্য memory-expensive হতে পারে
- Buffered streaming bounded memory ব্যবহার করে
InputStream.read()EOF-এ-1return করে- Buffer reads-এর ক্ষেত্রে
bytesReadrespect করা essential transferTo()direct stream copying simplify করেBufferedInputStreamএবংBufferedOutputStreamunderlying I/O calls reduce করতে পারেBufferedReaderline-oriented text processing-এর জন্য usefulBufferedWriterlarge incremental text output-এর জন্য usefulflush()buffered output force করতে পারে- Try-with-resources stream cleanup নিশ্চিত করে
- Resource ownership clear হওয়া উচিত
- Caller-owned resources unexpectedly close করা উচিত নয়
- Large output one giant string হিসেবে build করার প্রয়োজন নেই
- Character-based transformation exact binary copy নয়
- High-level APIs such as
Files.copy()preferred when no custom streaming logic is required - Buffering is a useful optimization, but it does not replace measurement or correct I/O design
Next Lesson
পরবর্তী lesson:
Managing Resources with Try-With-Resources
আমরা শিখব:
AutoCloseable- Resource ownership
- Automatic closing order
- Multiple resources
- Exceptions during
close() - Suppressed exceptions
- Custom
AutoCloseableresources - Why
finally-based cleanup is more error-prone - Designing safe resource APIs