File Handling and I/O

Working with Streams and Buffers

ReadingPreview

আপনি একটি 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 কী
  • InputStream
  • OutputStream
  • Reader
  • Writer
  • BufferedInputStream
  • BufferedOutputStream
  • BufferedReader
  • BufferedWriter
  • 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:

  1. PNG image
  2. Markdown lesson
  3. ZIP archive
  4. CSV file
  5. PDF
  6. 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 এবং OutputStream byte-oriented I/O
  • Reader এবং Writer character-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-এ -1 return করে
  • Buffer reads-এর ক্ষেত্রে bytesRead respect করা essential
  • transferTo() direct stream copying simplify করে
  • BufferedInputStream এবং BufferedOutputStream underlying I/O calls reduce করতে পারে
  • BufferedReader line-oriented text processing-এর জন্য useful
  • BufferedWriter large incremental text output-এর জন্য useful
  • flush() 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 AutoCloseable resources
  • Why finally-based cleanup is more error-prone
  • Designing safe resource APIs