File Handling and I/O

Managing Resources with Try-With-Resources

ReadingPreview

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

Lesson Overview

File, stream, database connection, socket—এগুলো সাধারণ object-এর মতো হলেও একটি extra responsibility আছে:

Use করার পর release বা close করতে হয়

Examples:

InputStream
OutputStream
BufferedReader
BufferedWriter

Resource properly close না করলে:

  • File descriptor leak হতে পারে
  • File lock unnecessarily ধরে রাখা হতে পারে
  • Memory বা native resource leak হতে পারে
  • Application eventually new resources open করতে ব্যর্থ হতে পারে

Java এই problem solve করার জন্য provide করে:

try-with-resources

এটি কাজ করে এমন objects-এর সঙ্গে যারা implement করে:

AutoCloseable

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

  • Resource কী
  • AutoCloseable
  • Try-with-resources syntax
  • Automatic closing
  • Multiple resources
  • Closing order
  • Exception during close()
  • Suppressed exceptions
  • Resource ownership
  • Custom AutoCloseable
  • finally-based cleanup-এর সমস্যা
  • Safe resource API design

Learning Objectives

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

  • Resource lifecycle explain করতে
  • AutoCloseable বুঝতে
  • Try-with-resources লিখতে
  • Multiple resources safely manage করতে
  • Closing order predict করতে
  • Suppressed exception inspect করতে
  • Custom closeable resource তৈরি করতে
  • Resource ownership define করতে
  • Manual cleanup-এর common bugs avoid করতে

What Is a Resource?

A resource এমন কিছু যা application ব্যবহার করে এবং পরে release করা প্রয়োজন।

Examples:

File stream
Reader
Writer
Database connection
Network socket
Directory stream
Lock-backed handle

Resource often refers to something outside ordinary Java heap memory.

Example:

InputStream input =
        Files.newInputStream(
                path
        );

The JVM object exists in memory, but it may also hold:

Operating-system file descriptor

Why Closing Matters

Consider:

InputStream input =
        Files.newInputStream(
                path
        );

process(
        input
);

If input is never closed, the underlying file handle may remain open longer than intended.

Repeated many times:

Open file
Forget close
Open file
Forget close
...

eventually the process can run out of file descriptors.


Manual close()

Basic approach:

InputStream input =
        Files.newInputStream(
                path
        );

process(
        input
);

input.close();

This looks fine until:

process(
        input
);

throws an exception.

Then:

input.close();

never executes.


The Cleanup Problem

InputStream input =
        Files.newInputStream(
                path
        );

process(
        input
);

input.close();

Normal flow:

Open
Process
Close

Exceptional flow:

Open
Process throws
Close skipped

This is exactly why resource cleanup needs a stronger mechanism.


Traditional finally Cleanup

Before try-with-resources, common pattern:

InputStream input =
        null;

try {
    input =
            Files.newInputStream(
                    path
            );

    process(
            input
    );
} finally {
    if (input != null) {
        input.close();
    }
}

This is better because finally runs during exceptional flow.

But it still has problems.


Problem: close() Can Also Throw

Suppose:

process() throws IOException A

Then:

input.close() throws IOException B

Which exception should caller see?

Naive finally code may replace the original processing failure with the close failure.

That hides the real cause.


Try-With-Resources

Modern Java provides:

try (
        InputStream input =
                Files.newInputStream(
                        path
                )
) {
    process(
            input
    );
}

When the block finishes, Java automatically calls:

input.close();

This happens when the block:

  • Completes normally
  • Returns
  • Throws an exception

Basic Example

public static void printFile(
        Path path
) {
    try (
            BufferedReader reader =
                    Files.newBufferedReader(
                            path,
                            StandardCharsets.UTF_8
                    )
    ) {
        String line;

        while (
                (
                        line =
                                reader.readLine()
                )
                != null
        ) {
            System.out.println(
                    line
            );
        }
    } catch (
            IOException exception
    ) {
        throw new IllegalStateException(
                "Could not read file.",
                exception
        );
    }
}

No manual:

reader.close();

required.


What Makes an Object Compatible?

A resource must implement:

AutoCloseable

Simplified interface:

public interface AutoCloseable {

    void close()
            throws Exception;
}

Any compatible object can be declared inside try-with-resources.


Closeable

There is also:

java.io.Closeable

Closeable extends:

AutoCloseable

Many I/O classes implement Closeable.

Examples:

InputStream
OutputStream
Reader
Writer
BufferedReader
BufferedWriter

Automatic Closing

This:

try (
        InputStream input =
                Files.newInputStream(
                        path
                )
) {
    process(
            input
    );
}

is conceptually similar to:

Open resource
↓
Execute body
↓
Close resource automatically

But Java also handles exception preservation correctly.


Return Still Closes the Resource

public static String firstLine(
        Path path
) throws IOException {
    try (
            BufferedReader reader =
                    Files.newBufferedReader(
                            path,
                            StandardCharsets.UTF_8
                    )
    ) {
        return reader.readLine();
    }
}

Before method actually returns, Java closes:

reader

Exception Still Closes the Resource

try (
        BufferedReader reader =
                Files.newBufferedReader(
                        path
                )
) {
    throw new IllegalStateException(
            "Processing failed."
    );
}

The reader is still closed.

Then the exception propagates.


Multiple Resources

You can declare multiple resources:

try (
        InputStream input =
                Files.newInputStream(
                        source
                );

        OutputStream output =
                Files.newOutputStream(
                        target
                )
) {
    input.transferTo(
            output
    );
}

Both are automatically closed.


Closing Order

Resources close in reverse declaration order.

Declared:

input
output

Actually close:

output
input

This is similar to stack behavior:

Last opened
First closed

Why Reverse Order Makes Sense

Suppose one resource depends on another.

Example:

OutputStream output =
        ...;

Writer writer =
        new OutputStreamWriter(
                output,
                StandardCharsets.UTF_8
        );

You should close:

Writer first
OutputStream second

because writer may need to flush data into the underlying output stream.


Layered Resources

Example:

try (
        OutputStream output =
                Files.newOutputStream(
                        path
                );

        BufferedWriter writer =
                new BufferedWriter(
                        new OutputStreamWriter(
                                output,
                                StandardCharsets.UTF_8
                        )
                )
) {
    writer.write(
            "Java"
    );
}

Closing order:

writer
output

Do You Need to Declare Every Wrapper Separately?

Often no.

Simpler:

try (
        BufferedWriter writer =
                Files.newBufferedWriter(
                        path,
                        StandardCharsets.UTF_8
                )
) {
    writer.write(
            "Java"
    );
}

Closing BufferedWriter closes its underlying writer and stream.

Prefer the simplest correct API.


Exception During the Try Body

Suppose:

try (
        TestResource resource =
                new TestResource()
) {
    throw new IllegalStateException(
            "Processing failed."
    );
}

Then:

resource.close()

also throws.

Java needs to preserve both failures.


Primary Exception

The exception thrown from the main try body becomes the primary exception.

Example:

IllegalStateException: Processing failed

The close failure is attached to it as:

suppressed exception

Suppressed Exceptions

Suppose:

Primary:
ProcessingException

Close failure:
IOException

Java keeps:

ProcessingException

as the main failure.

The close failure is available through:

exception.getSuppressed()

Inspecting Suppressed Exceptions

try {
    useResource();
} catch (
        Exception exception
) {
    for (
            Throwable suppressed
            : exception.getSuppressed()
    ) {
        System.out.println(
                "Suppressed: "
                + suppressed.getMessage()
        );
    }
}

Why Suppressed Exceptions Matter

Without suppression support, cleanup failure could hide the actual operation failure.

Try-with-resources preserves:

What originally failed
+
What also failed during cleanup

This is one of its biggest advantages over naive finally.


Custom AutoCloseable

You can create your own resource.

public final class CourseSession
        implements AutoCloseable {

    private final String courseCode;
    private boolean closed;

    public CourseSession(
            String courseCode
    ) {
        if (
                courseCode == null
                || courseCode.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        this.courseCode =
                courseCode.strip();

        this.closed =
                false;

        System.out.println(
                "Opened session for "
                + this.courseCode
        );
    }

    public void process() {
        if (closed) {
            throw new IllegalStateException(
                    "Session is already closed."
            );
        }

        System.out.println(
                "Processing "
                + courseCode
        );
    }

    @Override
    public void close() {
        if (closed) {
            return;
        }

        closed =
                true;

        System.out.println(
                "Closed session for "
                + courseCode
        );
    }
}

Using the Custom Resource

try (
        CourseSession session =
                new CourseSession(
                        "JAVA-OOP"
                )
) {
    session.process();
}

Possible output:

Opened session for JAVA-OOP
Processing JAVA-OOP
Closed session for JAVA-OOP

Idempotent close()

Notice:

if (closed) {
    return;
}

Calling:

close()

more than once does nothing.

This is often a useful resource design.

A close method should generally avoid damaging state if accidentally invoked repeatedly.


Closed Resource State

After:

close()

the resource should usually reject operations.

Example:

if (closed) {
    throw new IllegalStateException(
            "Session is already closed."
    );
}

This prevents silent use-after-close bugs.


close() Should Be Focused

Avoid:

public void close() {
    sendEmail();
    callPaymentService();
    rebuildCatalog();
}

close() should primarily release resources or finalize resource-specific work.

Complex business operations do not belong there.


Avoid Hiding Important Failures in close()

If cleanup can fail meaningfully:

@Override
public void close()
        throws IOException {
}

Try-with-resources can preserve that failure.

Do not silently ignore serious cleanup errors unless the contract explicitly allows it.


Resource Ownership

A fundamental question:

Who is responsible for closing the resource?

A good default:

Who opens it, owns it.

Example:

public void importFile(
        Path path
) {
    try (
            InputStream input =
                    Files.newInputStream(
                            path
                    )
    ) {
        process(
                input
        );
    }
}

importFile() opened the stream, so it closes it.


Caller-Owned Resource

Suppose:

public void process(
        InputStream input
) throws IOException {
}

The method did not open the stream.

Usually caller retains ownership.

Caller:

try (
        InputStream input =
                Files.newInputStream(
                        path
                )
) {
    processor.process(
            input
    );
}

process() should not unexpectedly close it unless documented.


Why Ownership Matters

Suppose:

processor.process(
        input
);

readMore(
        input
);

If process() secretly closes the stream, readMore() fails.

Clear ownership prevents surprises.


Returning a Resource

A method can deliberately transfer ownership:

public InputStream openContent(
        Path path
) throws IOException {
    return Files.newInputStream(
            path
    );
}

Caller:

try (
        InputStream input =
                storage.openContent(
                        path
                )
) {
    process(
            input
    );
}

Here caller becomes responsible for closing it.


Do Not Close Before Returning

Wrong:

public InputStream openContent(
        Path path
) throws IOException {
    try (
            InputStream input =
                    Files.newInputStream(
                            path
                    )
    ) {
        return input;
    }
}

The returned stream is already closed.


Existing Variables in Try-With-Resources

Modern Java can use an effectively final variable:

BufferedReader reader =
        Files.newBufferedReader(
                path
        );

try (
        reader
) {
    System.out.println(
            reader.readLine()
    );
}

After the block, reader exists as a variable but the underlying resource is closed.


Do Not Reuse the Closed Resource

BufferedReader reader =
        Files.newBufferedReader(
                path
        );

try (
        reader
) {
    System.out.println(
            reader.readLine()
    );
}

reader.readLine();

The final call fails because the reader has been closed.

Variable scope and resource lifecycle are different concepts.


Nested Try-With-Resources

Possible:

try (
        InputStream input =
                Files.newInputStream(
                        source
                )
) {
    try (
            OutputStream output =
                    Files.newOutputStream(
                            target
                    )
    ) {
        input.transferTo(
                output
        );
    }
}

But this is usually less readable than:

try (
        InputStream input =
                Files.newInputStream(
                        source
                );

        OutputStream output =
                Files.newOutputStream(
                        target
                )
) {
    input.transferTo(
            output
    );
}

Resource Initialization Can Fail

Consider:

try (
        InputStream input =
                openInput();

        OutputStream output =
                openOutput()
) {
}

What if:

input

opens successfully but:

output

creation throws?

Java automatically closes already-created:

input

before propagating the initialization failure.

This is another major safety benefit.


Closing Order Example

public final class DemoResource
        implements AutoCloseable {

    private final String name;

    public DemoResource(
            String name
    ) {
        this.name = name;

        System.out.println(
                "Open "
                + name
        );
    }

    @Override
    public void close() {
        System.out.println(
                "Close "
                + name
        );
    }
}

Usage:

try (
        DemoResource first =
                new DemoResource(
                        "A"
                );

        DemoResource second =
                new DemoResource(
                        "B"
                );

        DemoResource third =
                new DemoResource(
                        "C"
                )
) {
    System.out.println(
            "Use resources"
    );
}

Output:

Open A
Open B
Open C
Use resources
Close C
Close B
Close A

Manual finally Is More Error-Prone

Manual:

InputStream input =
        null;

try {
    input =
            Files.newInputStream(
                    path
            );

    process(
            input
    );
} finally {
    if (input != null) {
        input.close();
    }
}

Problems:

  • More code
  • Null handling
  • Close can throw
  • Original exception may be obscured
  • Multiple resources become complicated

Try-with-resources handles all of these better.


Multiple Resources with Manual Cleanup

Imagine:

InputStream input;
OutputStream output;
BufferedWriter writer;

You must close in reverse order and preserve failures correctly.

This becomes tedious and fragile.

Try-with-resources expresses lifecycle directly in syntax.


finally Still Has Uses

Try-with-resources is specifically for closeable resources.

finally can still be useful for non-resource cleanup:

Release a lock
Restore temporary state
Clear thread-local context
Stop timing measurement

Example:

lock.lock();

try {
    update();
} finally {
    lock.unlock();
}

A lock is not necessarily modeled with AutoCloseable.


Custom Lock Resource

Sometimes you can create an abstraction:

public final class LockHandle
        implements AutoCloseable {

    private final Lock lock;

    public LockHandle(
            Lock lock
    ) {
        this.lock = lock;
        this.lock.lock();
    }

    @Override
    public void close() {
        lock.unlock();
    }
}

Then:

try (
        LockHandle ignored =
                new LockHandle(
                        lock
                )
) {
    update();
}

This can make ownership explicit, but only introduce such abstractions when they genuinely improve design.


Exceptions from close()

A custom AutoCloseable can declare:

@Override
public void close()
        throws Exception {
}

But broad Exception can make APIs awkward.

When possible, use a more specific exception type.

Example:

@Override
public void close()
        throws IOException {
}

AutoCloseable vs Closeable

Simplified distinction:

AutoCloseable.close()
    may throw Exception

Closeable.close()
    throws IOException

Closeable is specialized for many I/O resources.

For custom general-purpose resources, AutoCloseable is often sufficient.


A Resource Should Not Be Shared Carelessly

Suppose two components share one writer:

Service A
Service B
↓
Same BufferedWriter

If Service A closes it, Service B breaks.

Resource sharing requires explicit lifecycle ownership.

Prefer:

One clear owner

over uncertain shared ownership.


Long-Lived Resources

Not every resource should be opened and closed per method call.

Examples:

Connection pool
Application-level HTTP client
Thread pool

These may live for the application lifetime and close during application shutdown.

The same ownership principle still applies:

The component that owns lifecycle is responsible for closing it.

Resource Factory Pattern

A component may create short-lived resources:

public InputStream open(
        CourseCode courseCode
)

Caller owns each returned stream.

Alternatively, hide resource management:

public String read(
        CourseCode courseCode
)

The storage component opens, reads, closes, then returns data.

The second design is often simpler for callers.


Prefer APIs That Hide Resource Management When Possible

Compare:

InputStream input =
        storage.open(
                courseCode
        );

Caller must remember to close it.

Versus:

String content =
        storage.read(
                courseCode
        );

Storage handles lifecycle internally.

Use open-resource APIs only when streaming is genuinely needed.


Streaming APIs Need Clear Ownership

For large files:

InputStream openContent(
        CourseCode courseCode
)

may be appropriate.

Document:

Caller must close the returned stream.

This is part of the API contract.


Do Not Hide Close Failures by Returning from finally

Bad:

try {
    process();
} finally {
    return;
}

This can suppress:

  • Processing exception
  • Close exception
  • Actual failure signal

Avoid control flow from finally.


Complete Example: Course Content Import

package io.liveklass.content;

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public final class CourseContentImporter {

    public int countNonBlankLines(
            Path path
    ) {
        if (path == null) {
            throw new IllegalArgumentException(
                    "Content path is required."
            );
        }

        try (
                BufferedReader reader =
                        Files.newBufferedReader(
                                path,
                                StandardCharsets.UTF_8
                        )
        ) {
            int count =
                    0;

            String line;

            while (
                    (
                            line =
                                    reader.readLine()
                    )
                    != null
            ) {
                if (!line.isBlank()) {
                    count++;
                }
            }

            return count;
        } catch (
                IOException exception
        ) {
            throw new CourseContentImportException(
                    "Could not import course content.",
                    exception
            );
        }
    }
}

Custom Import Exception

package io.liveklass.content;

public final class CourseContentImportException
        extends RuntimeException {

    public CourseContentImportException(
            String message,
            Throwable cause
    ) {
        super(
                message,
                cause
        );
    }
}

Design Review

Who Opens the Reader?

countNonBlankLines()

does.

Therefore it owns and closes the reader.

Why Try-With-Resources?

Reader closes on:

  • Success
  • Return
  • Parsing failure
  • I/O failure

Why Translate IOException?

Higher layers see course-import semantics instead of raw storage implementation details.


Common Mistakes

Forgetting to Close a Resource

Can leak operating-system resources.


Closing Only on the Happy Path

Exceptions can skip manual close() calls.


Returning a Resource from Inside Its Try-With-Resources Block

The caller receives a closed resource.


Closing Caller-Owned Resources

Breaks ownership expectations.


Sharing One Resource Without Clear Ownership

One component may close it while another still needs it.


Using Manual finally When Try-With-Resources Fits

Adds avoidable complexity.


Forgetting Reverse Close Order

Dependent resources should close from outermost to underlying resource.


Ignoring Suppressed Exceptions

Can hide useful cleanup diagnostics.


Throwing from finally

May replace the original failure.


Returning from finally

May suppress exceptions entirely.


Making close() Perform Business Operations

Resource cleanup should remain focused.


Practice Exercises

Exercise 1: Read a File Safely

Create:

String readFirstLine(
        Path path
)

Use:

BufferedReader
try-with-resources

Translate IOException while preserving the cause.


Exercise 2: Copy Two Streams

Open:

InputStream
OutputStream

inside one try-with-resources block.

Copy using:

transferTo()

Explain the close order.


Exercise 3: Custom Resource

Create:

ProcessingSession
        implements AutoCloseable

Requirements:

  • Print when opened
  • Reject work after close
  • close() may be called more than once safely

Exercise 4: Predict Closing Order

Resources declared:

A
B
C
D

What is the closing order?


Exercise 5: Ownership

Method:

void process(
        InputStream input
)

receives a caller-created stream.

Should it close it automatically?

Explain the default ownership rule.


Exercise 6: Suppressed Failure

Create a custom resource whose:

close()

throws an exception.

Inside the try body, throw another exception.

Inspect:

getSuppressed()

Exercise 7: Fix the Bug

public InputStream open(
        Path path
) throws IOException {
    try (
            InputStream input =
                    Files.newInputStream(
                            path
                    )
    ) {
        return input;
    }
}

Explain what is wrong and correct the method.


Predict the Result

Question 1

try (
        DemoResource a =
                new DemoResource(
                        "A"
                );

        DemoResource b =
                new DemoResource(
                        "B"
                )
) {
    System.out.println(
            "Work"
    );
}

What is the close order?

Answer

B
A

Question 2

A resource is opened inside try-with-resources and the method returns from inside the block.

Is the resource closed?

Answer

Yes.

It is closed before the method completes the return.


Question 3

The try body throws exception A and close() throws exception B.

Which one is primary?

Answer

Exception A.

Exception B becomes a suppressed exception.


Question 4

Can any arbitrary Java object be used in try-with-resources?

Answer

No.

It must implement:

AutoCloseable

or a subtype such as:

Closeable

Question 5

A method opens an InputStream, fully processes it, and does not return it.

Who should normally close it?

Answer

The method itself, because it owns the resource lifecycle.


Knowledge Check

Question 1

What is a resource?

Question 2

Why must resources be closed?

Question 3

What is AutoCloseable?

Question 4

What does try-with-resources guarantee?

Question 5

In what order are multiple resources closed?

Question 6

What happens when the try body throws and close() also throws?

Question 7

What is a suppressed exception?

Question 8

How can suppressed exceptions be inspected?

Question 9

Why is try-with-resources safer than naive finally cleanup?

Question 10

What is resource ownership?

Question 11

Who should normally close a resource?

Question 12

Why should a method not return a resource created inside its own try-with-resources block?

Question 13

Can a custom class participate in try-with-resources?

Question 14

Why should close() usually be focused and idempotent?

Question 15

When might finally still be appropriate?


Knowledge Check Answers

Answer 1

An object that owns an external or limited resource requiring explicit release after use.

Answer 2

To release file descriptors, buffers, connections, locks, or other underlying resources promptly.

Answer 3

An interface with a close() contract that makes objects compatible with try-with-resources.

Answer 4

Declared resources are automatically closed when the block finishes normally or exceptionally.

Answer 5

Reverse declaration order.

Answer 6

The body exception remains primary and the close exception is attached as suppressed.

Answer 7

A secondary failure preserved while another exception remains the primary failure.

Answer 8

Using:

exception.getSuppressed()

Answer 9

It handles closing, reverse order, initialization failures, and suppressed exceptions automatically.

Answer 10

The contract defining which component is responsible for closing a resource.

Answer 11

Usually the component that opens and owns it.

Answer 12

The resource is closed before the caller receives it.

Answer 13

Yes, by implementing AutoCloseable.

Answer 14

Focused cleanup keeps lifecycle predictable; idempotent close avoids damage from repeated close calls.

Answer 15

For cleanup that is not represented by an AutoCloseable resource, such as unlocking or restoring temporary state.


Lesson Summary

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

  • Resources often wrap external or limited system resources
  • Resources should be released promptly
  • Manual happy-path close() is unsafe when exceptions occur
  • AutoCloseable enables try-with-resources
  • Closeable is an I/O-focused subtype of AutoCloseable
  • Try-with-resources closes resources automatically
  • Resources close on success, return, or exception
  • Multiple resources close in reverse declaration order
  • Already-created resources are closed if later resource initialization fails
  • Try-body exceptions remain primary
  • Cleanup failures become suppressed exceptions
  • getSuppressed() exposes suppressed failures
  • Custom classes can implement AutoCloseable
  • Resource ownership must be explicit
  • The component that opens a resource usually closes it
  • Caller-owned resources should not be closed unexpectedly
  • Returning a resource from inside its own try-with-resources block returns a closed resource
  • close() should remain focused and preferably safe to call repeatedly
  • Manual finally cleanup is more error-prone
  • finally is still useful for non-resource cleanup
  • High-level APIs that hide resource management are preferable when streaming is unnecessary
  • Streaming APIs must clearly document caller ownership

Next Lesson

পরবর্তী lesson:

File and Directory Operations

আমরা শিখব:

  • Creating files and directories
  • Copying files
  • Moving and renaming
  • Deleting files
  • delete() vs deleteIfExists()
  • Directory listing
  • Recursive traversal
  • Files.walk()
  • File replacement options
  • Safe cleanup
  • Common file-system race conditions