File Handling and I/O

Understanding Files, Paths, and Directories

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

Lesson Overview

Java application অনেক সময় file system-এর সঙ্গে কাজ করে।

Examples:

Course content load করা
Configuration file পড়া
Generated report save করা
Uploaded file process করা
Temporary file তৈরি করা
Directory scan করা
Logs বা exports manage করা

Modern Java-তে file system নিয়ে কাজ করার primary APIs হলো:

Path
Files

এগুলো পাওয়া যায়:

java.nio.file

এই lesson-এ আমরা শিখব:

  • File path কী
  • Path কী
  • Files utility class
  • Relative path
  • Absolute path
  • Path normalization
  • File এবং directory distinguish করা
  • Existence checks
  • File metadata
  • Parent, file name, extension
  • Directory contents inspect করা
  • Cross-platform path handling
  • Path validation-এর practical rules

এই lesson-এ আমরা এখনও full file reading/writing করব না।

প্রথমে file system-এর structure এবং path model ঠিকভাবে বুঝব।


Learning Objectives

এই lesson শেষে আপনি পারবেন:

  • Path object তৈরি করতে
  • Relative এবং absolute path distinguish করতে
  • Path components inspect করতে
  • Path safely combine করতে
  • Normalize করতে
  • File existence check করতে
  • File এবং directory distinguish করতে
  • Basic metadata read করতে
  • Cross-platform path code লিখতে
  • String concatenation দিয়ে path বানানোর সমস্যা explain করতে

What Is a File Path?

A file path file system-এর একটি location represent করে।

Example:

courses/java/module-1.md

আরেকটি:

/home/sakib/liveklass/course.md

Windows example:

C:\Users\Sakib\liveklass\course.md

Path বলে:

File বা directory কোথায় আছে?

Path Is Not the File Content

এই দুইটি আলাদা concept:

Path
File content

Example:

Path path =
        Path.of(
                "course.md"
        );

এখানে file read করা হয়নি।

শুধু:

course.md

location represent করা হয়েছে।


Path

Modern Java file API-এর central abstraction:

java.nio.file.Path

Import:

import java.nio.file.Path;

Create:

Path path =
        Path.of(
                "course.md"
        );

Path.of()

Java-তে path create করার common approach:

Path.of(...)

Example:

Path path =
        Path.of(
                "courses",
                "java",
                "lesson.md"
        );

Conceptually:

courses/java/lesson.md

Actual separator operating system অনুযায়ী handle হয়।


Do Not Build Paths with String Concatenation

Weak:

String path =
        "courses/"
        + courseCode
        + "/lesson.md";

Problems:

  • Platform-specific separator
  • Double slash
  • Missing slash
  • Harder composition
  • Harder normalization

Better:

Path path =
        Path.of(
                "courses",
                courseCode,
                "lesson.md"
        );

Cross-Platform Paths

Unix-like systems commonly use:

/

Windows commonly uses:

\

Avoid hardcoding:

"courses\\java\\lesson.md"

or:

"courses/java/lesson.md"

when constructing paths from multiple components।

Use:

Path.of(
        "courses",
        "java",
        "lesson.md"
);

Java uses the correct platform representation।


Relative Path

A relative path does not start from a file-system root।

Example:

Path path =
        Path.of(
                "courses",
                "java",
                "lesson.md"
        );

This is relative।

Meaning:

Start from the current working directory
then go to courses/java/lesson.md

Current Working Directory

When Java program runs, it has a working directory।

You can inspect it:

String currentDirectory =
        System.getProperty(
                "user.dir"
        );

System.out.println(
        currentDirectory
);

If output is:

/home/sakib/liveklass

Then relative path:

courses/java/lesson.md

refers to:

/home/sakib/liveklass/courses/java/lesson.md

Absolute Path

An absolute path begins from a file-system root।

Linux/macOS example:

/home/sakib/liveklass/course.md

Windows example:

C:\Users\Sakib\liveklass\course.md

Check:

Path path =
        Path.of(
                "/home/sakib/liveklass/course.md"
        );

System.out.println(
        path.isAbsolute()
);

Possible output:

true

Converting to Absolute Path

Path relativePath =
        Path.of(
                "courses",
                "java",
                "lesson.md"
        );

Path absolutePath =
        relativePath.toAbsolutePath();

System.out.println(
        absolutePath
);

The result depends on the current working directory।


toAbsolutePath() Does Not Guarantee the File Exists

Important:

Path absolute =
        Path.of(
                "missing.txt"
        ).toAbsolutePath();

This can produce a perfectly valid absolute path even if:

missing.txt does not exist

Path construction and file existence are different concerns।


getFileName()

Path path =
        Path.of(
                "courses",
                "java",
                "lesson-1.md"
        );

System.out.println(
        path.getFileName()
);

Output:

lesson-1.md

getParent()

Path path =
        Path.of(
                "courses",
                "java",
                "lesson-1.md"
        );

System.out.println(
        path.getParent()
);

Possible output:

courses/java

getRoot()

For relative path:

Path path =
        Path.of(
                "courses",
                "java"
        );

System.out.println(
        path.getRoot()
);

Result:

null

because relative path has no root।

For absolute Unix path:

Path path =
        Path.of(
                "/home/sakib"
        );

Root is:

/

Path Components

Path path =
        Path.of(
                "courses",
                "java",
                "module-1",
                "lesson.md"
        );

Number of name components:

System.out.println(
        path.getNameCount()
);

Output:

4

Components:

courses
java
module-1
lesson.md

Access a Path Component

System.out.println(
        path.getName(
                0
        )
);

Output:

courses

Path Index Starts at Zero

Given:

courses/java/lesson.md

Components:

0 → courses
1 → java
2 → lesson.md

Same zero-based indexing principle as List and arrays।


resolve()

One of the most useful Path operations:

resolve()

It combines paths।

Example:

Path courseDirectory =
        Path.of(
                "courses",
                "java"
        );

Path lessonPath =
        courseDirectory.resolve(
                "lesson-1.md"
        );

Result:

courses/java/lesson-1.md

Resolve Another Path

Path base =
        Path.of(
                "courses"
        );

Path child =
        Path.of(
                "java",
                "lesson.md"
        );

Path result =
        base.resolve(
                child
        );

Result:

courses/java/lesson.md

Why resolve() Is Better Than Concatenation

Weak:

String result =
        base
        + "/"
        + child;

Better:

Path result =
        base.resolve(
                child
        );

The API understands file-system path semantics।


Absolute Path with resolve()

Important behavior:

Path base =
        Path.of(
                "/home/sakib"
        );

Path other =
        Path.of(
                "/tmp/course.md"
        );

Path result =
        base.resolve(
                other
        );

Because other is absolute, result becomes:

/tmp/course.md

The base is ignored।

This matters when resolving external input।


normalize()

Paths can contain:

.
..

Example:

Path path =
        Path.of(
                "courses",
                "java",
                "..",
                "backend",
                ".",
                "lesson.md"
        );

Normalize:

Path normalized =
        path.normalize();

Result:

courses/backend/lesson.md

Meaning of .

.

means:

Current directory

Example:

courses/./java

normalizes to:

courses/java

Meaning of ..

..

means:

Parent directory

Example:

courses/java/../backend

normalizes to:

courses/backend

Normalize Does Not Access the File System

path.normalize()

is a path operation।

It does not verify:

  • File exists
  • Parent exists
  • Symbolic link target
  • Permissions

It simply simplifies path components।


relativize()

Suppose:

Path base =
        Path.of(
                "/home/sakib/liveklass"
        );

Path target =
        Path.of(
                "/home/sakib/liveklass/courses/java.md"
        );

Then:

Path relative =
        base.relativize(
                target
        );

Result:

courses/java.md

relativize() Requires Compatible Paths

Usually both paths should be:

Both absolute

or:

Both relative

Mixing absolute and relative paths can fail।


The Files Utility Class

Path represents a location।

Files performs file-system operations।

Import:

import java.nio.file.Files;

Examples:

Files.exists(path)
Files.isDirectory(path)
Files.isRegularFile(path)
Files.size(path)
Files.createDirectory(path)

Checking Whether a Path Exists

Path path =
        Path.of(
                "course.md"
        );

boolean exists =
        Files.exists(
                path
        );

System.out.println(
        exists
);

Files.notExists()

boolean missing =
        Files.notExists(
                path
        );

Important:

!Files.exists(path)

and:

Files.notExists(path)

are not conceptually identical in every edge case।

Why?

Because file existence may sometimes be:

Unknown

due to permission or I/O issues।

For beginner use, Files.exists() is usually sufficient।


File or Directory?

Regular file:

Files.isRegularFile(
        path
);

Directory:

Files.isDirectory(
        path
);

Example:

Path path =
        Path.of(
                "courses"
        );

if (
        Files.isDirectory(
                path
        )
) {
    System.out.println(
            "This is a directory."
    );
}

Existence Alone Is Not Enough

Weak:

if (
        Files.exists(
                path
        )
) {
    // Assume file
}

The path may point to:

Directory
Symbolic link
Special file

If you specifically require a regular file:

if (
        Files.isRegularFile(
                path
        )
) {
}

Directory Existence

Path courseDirectory =
        Path.of(
                "courses"
        );

if (
        !Files.isDirectory(
                courseDirectory
        )
) {
    System.out.println(
            "Course directory is unavailable."
    );
}

Basic File Metadata

Java can inspect file metadata।

Examples:

Size
Last modified time
Creation time
Is directory
Is regular file

File Size

long size =
        Files.size(
                path
        );

This method can throw:

IOException

Example:

try {
    long size =
            Files.size(
                    path
            );

    System.out.println(
            size
    );
} catch (
        IOException exception
) {
    System.out.println(
            "Could not read file size."
    );
}

Last Modified Time

FileTime lastModified =
        Files.getLastModifiedTime(
                path
        );

Imports:

import java.nio.file.attribute.FileTime;

Example:

System.out.println(
        lastModified
);

Basic File Attributes

For multiple metadata fields:

BasicFileAttributes attributes =
        Files.readAttributes(
                path,
                BasicFileAttributes.class
        );

Import:

import java.nio.file.attribute.BasicFileAttributes;

Then:

attributes.size();
attributes.creationTime();
attributes.lastModifiedTime();
attributes.isDirectory();
attributes.isRegularFile();

Example Metadata Method

public static void printMetadata(
        Path path
) {
    try {
        BasicFileAttributes attributes =
                Files.readAttributes(
                        path,
                        BasicFileAttributes.class
                );

        System.out.println(
                "Size: "
                + attributes.size()
        );

        System.out.println(
                "Created: "
                + attributes.creationTime()
        );

        System.out.println(
                "Modified: "
                + attributes.lastModifiedTime()
        );

        System.out.println(
                "Directory: "
                + attributes.isDirectory()
        );

        System.out.println(
                "Regular file: "
                + attributes.isRegularFile()
        );
    } catch (
            IOException exception
    ) {
        throw new IllegalStateException(
                "Could not read file metadata.",
                exception
        );
    }
}

File Extension

Path does not have a built-in:

getExtension()

method।

You can inspect the file name:

String fileName =
        path.getFileName()
                .toString();

Then:

int dotIndex =
        fileName.lastIndexOf(
                '.'
        );

Example helper:

public static String getExtension(
        Path path
) {
    if (path == null) {
        throw new IllegalArgumentException(
                "Path is required."
        );
    }

    Path fileNamePath =
            path.getFileName();

    if (fileNamePath == null) {
        return "";
    }

    String fileName =
            fileNamePath.toString();

    int dotIndex =
            fileName.lastIndexOf(
                    '.'
            );

    if (
            dotIndex <= 0
            || dotIndex
            == fileName.length() - 1
    ) {
        return "";
    }

    return fileName.substring(
            dotIndex + 1
    );
}

Examples

lesson.md       → md
course.json     → json
archive.tar.gz  → gz
README          → ""
.env            → ""
file.           → ""

Whether .env should be considered an extension is a domain decision।


Directory Listing

To inspect direct children of a directory:

Files.list(
        directory
);

It returns:

Stream<Path>

Because the stream uses an underlying resource, use try-with-resources।

try (
        var paths =
                Files.list(
                        directory
                )
) {
    paths.forEach(
            System.out::println
    );
}

Why Try-With-Resources Here?

The stream returned by:

Files.list(...)

holds an open directory resource।

It should be closed।

try (...)

ensures cleanup।


List Only Regular Files

try (
        var paths =
                Files.list(
                        directory
                )
) {
    paths.filter(
                Files::isRegularFile
    ).forEach(
            System.out::println
    );
}

List Only Directories

try (
        var paths =
                Files.list(
                        directory
                )
) {
    paths.filter(
                Files::isDirectory
    ).forEach(
            System.out::println
    );
}

Files.list() Is Not Recursive

Suppose:

courses/
├── java/
│   └── lesson.md
└── backend/
    └── api.md

Calling:

Files.list(
        Path.of(
                "courses"
        )
)

returns direct children:

java
backend

It does not automatically return nested lesson files।

Recursive traversal will be covered later।


Checking Readability and Writability

Files.isReadable(
        path
);
Files.isWritable(
        path
);
Files.isExecutable(
        path
);

These can be useful hints।

But they do not guarantee a later operation will succeed।


Why Pre-Checks Cannot Guarantee Success

Weak assumption:

if (
        Files.isWritable(
                path
        )
) {
    // Writing must succeed
}

Between check and actual write:

  • Permissions may change
  • File may disappear
  • Disk may fill
  • Another process may lock/change it

The actual write operation must still handle failure।

This is a general rule:

Pre-checks improve intent and feedback, but the real operation remains authoritative.


Path Equality

Path first =
        Path.of(
                "courses",
                "java"
        );

Path second =
        Path.of(
                "courses",
                "java"
        );

System.out.println(
        first.equals(
                second
        )
);

Output:

true

Textually Different Paths May Refer to the Same Location

courses/java
courses/./java
courses/backend/../java

These may resolve to the same logical location after normalization।

Example:

Path first =
        Path.of(
                "courses",
                "java"
        );

Path second =
        Path.of(
                "courses",
                ".",
                "java"
        );

System.out.println(
        first.equals(
                second
        )
);

Likely:

false

But:

first.normalize()
        .equals(
                second.normalize()
        );

returns:

true

toRealPath()

toRealPath() resolves a path using the actual file system।

Path realPath =
        path.toRealPath();

It may:

  • Convert to absolute path
  • Resolve symbolic links
  • Remove redundant elements
  • Verify the path exists

It can throw:

IOException

normalize() vs toRealPath()

normalize()

Pure path manipulation
Does not require file existence
Does not resolve real symbolic links

toRealPath()

Uses actual file system
Path must generally exist
Resolves real path details
Can throw IOException

Path Traversal Risk

Suppose application stores course files under:

/data/courses

User provides:

../../secret.txt

Weak:

Path path =
        base.resolve(
                userInput
        );

This could escape the intended directory after normalization।


Safer Base-Directory Validation

Path baseDirectory =
        Path.of(
                "/data/courses"
        )
        .toAbsolutePath()
        .normalize();

Path target =
        baseDirectory.resolve(
                userInput
        )
        .normalize();

if (
        !target.startsWith(
                baseDirectory
        )
) {
    throw new IllegalArgumentException(
            "Path escapes the allowed directory."
    );
}

This is an important security pattern when path components come from external input।


Why normalize() Matters in Path Validation

Input:

java/../../secret.txt

Before normalization, it visually starts under:

java

After normalization:

../secret.txt

Now escape becomes obvious।


Do Not Accept Arbitrary File Paths When a File Name Is Enough

If user only needs to specify a course code:

Weak:

User sends full storage path

Better:

User sends course code
Application constructs path

Example:

Path courseFile =
        baseDirectory.resolve(
                courseCode.getValue()
                        + ".md"
        );

This gives the application stronger control over storage locations।


Paths as Domain Boundaries

Low-level file adapter may accept:

Path

But higher-level domain service may accept:

CourseCode

Example:

public String loadCourseContent(
        CourseCode courseCode
) {
    Path path =
            baseDirectory.resolve(
                    courseCode.getValue()
                    + ".md"
            );

    return storage.read(
            path
    );
}

This prevents file-system details from leaking throughout the domain।


Directory Structure Example

A simple LiveKlass content storage:

content/
├── java-oop/
│   ├── introduction.md
│   ├── oop.md
│   └── collections.md
└── backend/
    ├── http.md
    └── rest-api.md

Base:

Path contentDirectory =
        Path.of(
                "content"
        );

Course directory:

Path javaDirectory =
        contentDirectory.resolve(
                "java-oop"
        );

Lesson:

Path lesson =
        javaDirectory.resolve(
                "collections.md"
        );

Complete Example: Inspect a Course Directory

package io.liveklass;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;

public class Main {

    public static void main(
            String[] args
    ) {
        Path courseDirectory =
                Path.of(
                        "content",
                        "java-oop"
                );

        printPathInfo(
                courseDirectory
        );

        if (
                Files.isDirectory(
                        courseDirectory
                )
        ) {
            printChildren(
                    courseDirectory
            );
        }
    }

    private static void printPathInfo(
            Path path
    ) {
        System.out.println(
                "Path: "
                + path
        );

        System.out.println(
                "Absolute: "
                + path.isAbsolute()
        );

        System.out.println(
                "Absolute path: "
                + path.toAbsolutePath()
                        .normalize()
        );

        System.out.println(
                "File name: "
                + path.getFileName()
        );

        System.out.println(
                "Parent: "
                + path.getParent()
        );

        System.out.println(
                "Exists: "
                + Files.exists(
                        path
                )
        );

        System.out.println(
                "Directory: "
                + Files.isDirectory(
                        path
                )
        );
    }

    private static void printChildren(
            Path directory
    ) {
        try (
                var paths =
                        Files.list(
                                directory
                        )
        ) {
            paths.forEach(
                    Main::printChild
            );
        } catch (
                IOException exception
        ) {
            throw new IllegalStateException(
                    "Could not list course directory.",
                    exception
            );
        }
    }

    private static void printChild(
            Path path
    ) {
        System.out.println();
        System.out.println(
                "Child: "
                + path.getFileName()
        );

        try {
            BasicFileAttributes attributes =
                    Files.readAttributes(
                            path,
                            BasicFileAttributes.class
                    );

            System.out.println(
                    "Size: "
                    + attributes.size()
            );

            System.out.println(
                    "Regular file: "
                    + attributes.isRegularFile()
            );

            System.out.println(
                    "Directory: "
                    + attributes.isDirectory()
            );
        } catch (
                IOException exception
        ) {
            throw new IllegalStateException(
                    "Could not inspect "
                    + path
                    + ".",
                    exception
            );
        }
    }
}

Design Review

Why Use Path Instead of String?

Path communicates:

This value represents a file-system path.

It also provides:

  • resolve()
  • normalize()
  • getParent()
  • getFileName()
  • toAbsolutePath()

Why Use Files?

Path represents location।

Files performs file-system operations।

This separation is intentional।


Why Use Try-With-Resources for Files.list()?

The returned stream holds a directory resource that must be closed।


Why Wrap IOException?

Main example translates low-level I/O failure into a higher-level failure with context while preserving the cause।


Common Mistakes

Treating a Path as File Content

Creating a Path does not read anything।


Building Paths with Manual Separators

Can create portability and formatting issues।


Assuming Absolute Path Means Existing File

Absolute only describes location form।


Assuming normalize() Checks the File System

It only simplifies path components।


Assuming Files.exists() Means Regular File

The path may be a directory or another file type।


Forgetting to Close Files.list()

The returned stream uses a resource।


Trusting isWritable() as a Guarantee

The actual write can still fail।


Allowing Untrusted .. Components Without Validation

Can lead to path traversal outside the allowed directory।


Exposing Storage Paths to Domain Logic Everywhere

Creates file-system coupling।


Practice Exercises

Exercise 1: Build a Course Path

Create:

Path buildCoursePath(
        Path baseDirectory,
        String courseCode
)

Expected:

baseDirectory/java-oop

Do not use String path concatenation।


Exercise 2: Inspect a Path

Given:

Path path =
        Path.of(
                "content",
                "java",
                "lesson.md"
        );

Print:

  • File name
  • Parent
  • Name count
  • Absolute path
  • Whether it is absolute

Exercise 3: Normalize

Normalize:

content/java/../backend/./api.md

Predict the result before running the code।


Exercise 4: Validate Base Directory

Given:

Path base =
        Path.of(
                "/data/courses"
        );

Reject:

../../secret.txt

using:

resolve()
normalize()
startsWith()

Exercise 5: Directory Inspection

Given a directory, print only regular files using:

Files.list()
Files.isRegularFile()

Use try-with-resources।


Exercise 6: File Metadata

Write:

void printFileMetadata(
        Path path
)

Print:

  • Size
  • Creation time
  • Last modified time
  • Is regular file
  • Is directory

Preserve the cause if metadata loading fails।


Predict the Result

Question 1

Path path =
        Path.of(
                "courses",
                "java",
                "lesson.md"
        );

System.out.println(
        path.getFileName()
);

Answer

lesson.md

Question 2

Path path =
        Path.of(
                "courses",
                "java",
                "..",
                "backend"
        );

System.out.println(
        path.normalize()
);

Answer

Conceptually:

courses/backend

Question 3

Path path =
        Path.of(
                "missing.txt"
        );

Path absolute =
        path.toAbsolutePath();

Does this prove the file exists?

Answer

No।

It only creates an absolute representation of the path।


Question 4

Path base =
        Path.of(
                "/courses"
        );

Path absoluteChild =
        Path.of(
                "/tmp/test.md"
        );

System.out.println(
        base.resolve(
                absoluteChild
        )
);

Answer

/tmp/test.md

An absolute resolved path replaces the base।


Question 5

Does:

Files.exists(
        path
)

guarantee that:

Files.isRegularFile(
        path
)

is true?

Answer

No।

The existing path may represent a directory or another file type।


Knowledge Check

Question 1

What is a Path?

Question 2

What is the difference between Path and Files?

Question 3

What is a relative path?

Question 4

What is an absolute path?

Question 5

What does resolve() do?

Question 6

What does normalize() do?

Question 7

Does normalize() require the file to exist?

Question 8

What does getFileName() return?

Question 9

Why should path strings not be manually concatenated?

Question 10

How do you check whether a path is a directory?

Question 11

Why is Files.list() used with try-with-resources?

Question 12

What is the difference between normalize() and toRealPath()?

Question 13

Why is .. dangerous in untrusted paths?

Question 14

Does isWritable() guarantee a future write?

Question 15

Why might a service accept CourseCode instead of a raw storage Path?


Knowledge Check Answers

Answer 1

A Java object representing a file-system location।

Answer 2

Path represents a location; Files performs operations on file-system paths।

Answer 3

A path interpreted relative to the current working directory or another base path।

Answer 4

A path starting from the file system root।

Answer 5

It combines a base path with another path component।

Answer 6

It removes redundant . and resolvable .. path components।

Answer 7

No।

It is a path manipulation operation।

Answer 8

The last name component of the path।

Answer 9

Manual separators create portability, formatting, and composition problems।

Answer 10

Using:

Files.isDirectory(
        path
)

Answer 11

Because the returned stream holds an open directory resource that should be closed।

Answer 12

normalize() is purely lexical; toRealPath() consults the actual file system and can resolve symbolic links।

Answer 13

It may allow a resolved path to escape an intended base directory।

Answer 14

No।

The actual operation can still fail due to changing file-system conditions।

Answer 15

It keeps file-system implementation details out of higher-level domain APIs।


Lesson Summary

এই lesson-এ আমরা শিখেছি:

  • Path file-system location represent করে
  • Files file-system operations provide করে
  • Creating a Path does not access file contents
  • Relative paths current working directory-এর ওপর depend করে
  • Absolute paths file-system root থেকে শুরু হয়
  • toAbsolutePath() existence verify করে না
  • getFileName() final path component return করে
  • getParent() parent path return করে
  • resolve() safe path composition support করে
  • Manual path concatenation avoid করা উচিত
  • normalize() redundant . এবং .. components simplify করে
  • toRealPath() actual file system consult করে
  • Files.exists() existence check করে
  • Files.isRegularFile() এবং Files.isDirectory() path type distinguish করে
  • File metadata Files.readAttributes() দিয়ে পড়া যায়
  • Files.list() direct directory children stream করে
  • Files.list() result try-with-resources দিয়ে close করা উচিত
  • Pre-checks future file operation success guarantee করে না
  • Untrusted paths path traversal risk তৈরি করতে পারে
  • Resolved paths allowed base directory-এর মধ্যে আছে কি না validate করা উচিত
  • Strong domain values file-system pathsকে higher-level application logic থেকে hide করতে পারে

Next Lesson

পরবর্তী lesson:

Reading Text Files

আমরা শিখব:

  • Files.readString()
  • Files.readAllLines()
  • Files.lines()
  • BufferedReader
  • Small files vs large files
  • Character encoding
  • UTF-8
  • Streaming content
  • Resource handling
  • File read exception translation