File Handling and I/O

Reading Text Files

ReadingPreview

You are viewing a 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 করতে
  • IOException handle বা 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:

APILoads whole file?Best for
readString()YesEntire text content
readAllLines()YesSmall file as lines
Files.lines()No, streamedStream processing
BufferedReaderNo, streamedControlled 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:

  1. Load a 20 KB Markdown lesson as one string
  2. Read a 50-line configuration file into a list
  3. Search a 5 GB log file for the first matching line
  4. 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_8 makes encoding explicit
  • Files.readString() returns the entire file as one String
  • readString() is suitable for reasonably small files
  • Files.readAllLines() returns all lines as a List<String>
  • Both readString() and readAllLines() load the full file into memory
  • Files.lines() supports streamed line processing
  • Files.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
  • BufferedReader supports controlled sequential reading
  • readLine() returns null at end of file
  • BufferedReader is 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