Index

Working with Files and I/O

Java for Beginners

11.1 Reading and Writing Files (java.io, java.nio)

Working with files is a common requirement in many Java applications, whether you want to read data, write logs, or store configurations. Java provides two primary approaches for file input/output (I/O): the classic java.io package and the newer, more efficient java.nio (New I/O) package introduced in Java 7.

This section introduces both approaches, explaining their differences, and demonstrates how to read from and write to text files using practical examples.

Stream-Based I/O with java.io

The java.io package uses streams to handle data flow—either bytes (InputStream/OutputStream) or characters (Reader/Writer). For text files, character streams are most convenient.

Key Classes for Text File I/O:

Example: Reading and Writing Text Files Using java.io

import java.io.*;

public class FileIOExample {
    public static void main(String[] args) {
        String inputFile = "input.txt";
        String outputFile = "output.txt";

        // Writing to a file using FileWriter and BufferedWriter
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {
            writer.write("Hello, Java I/O!");
            writer.newLine();
            writer.write("This is a sample file.");
        } catch (IOException e) {
            System.err.println("Error writing file: " + e.getMessage());
        }

        // Reading from a file using FileReader and BufferedReader
        try (BufferedReader reader = new BufferedReader(new FileReader(outputFile))) {
            String line;
            System.out.println("Reading file content:");
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }
}

This program writes two lines to output.txt, then reads and prints them.

Buffer/Channel-Based I/O with java.nio

The java.nio package is designed for non-blocking, buffer-oriented I/O, offering better performance and scalability. Instead of streams, it uses buffers (containers for data) and channels (connections to entities like files or sockets).

Key Classes for File I/O in java.nio:

Simpler Reading and Writing with Files

Java 7 introduced convenient static methods in Files for reading and writing small files in one go.

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.List;

public class NIOExample {
    public static void main(String[] args) {
        Path filePath = Paths.get("nio_output.txt");

        // Writing lines to a file
        List<String> linesToWrite = List.of("Hello from NIO!", "Using Files and Paths.");
        try {
            Files.write(filePath, linesToWrite);
        } catch (IOException e) {
            System.err.println("Error writing file: " + e.getMessage());
        }

        // Reading lines from a file
        try {
            List<String> linesRead = Files.readAllLines(filePath);
            System.out.println("Contents of nio_output.txt:");
            for (String line : linesRead) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }
}

This approach is very concise and suitable for small to medium-sized text files.

Differences Between java.io and java.nio

Feature java.io (Stream-based) java.nio (Buffer/Channel-based)
Data Handling Reads/writes data one byte or character at a time Reads/writes data in chunks using buffers
Blocking I/O Blocking calls (waits for operation to complete) Supports non-blocking I/O (better for scalability)
Convenience Simple for basic file operations More powerful, requires more setup
Resource Management Needs explicit closing (try-with-resources helps) Uses channels and buffers, also requires closing
Performance Generally slower for large files Better performance and scalability

Exception Handling and Resource Management

File operations are prone to errors like missing files, permission issues, or I/O failures. Java enforces checked exceptions for many I/O methods, so you must handle or declare them.

The try-with-resources statement (Java 7+) automatically closes streams and readers after use, which is a best practice to avoid resource leaks.

Example of try-with-resources:

try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    // read file
} catch (IOException e) {
    // handle exception
}
// reader is closed automatically here

Summary

Mastering both approaches gives you the flexibility to handle a wide range of file I/O scenarios in Java, from small configuration files to large data streams.

Index

11.2 BufferedReader and BufferedWriter

When working with files in Java, efficiency is key—especially when reading or writing large amounts of data. Using buffered streams like BufferedReader and BufferedWriter helps improve performance by minimizing expensive I/O operations. In this section, we’ll explore what buffering means, why buffered streams matter, and how to use these classes properly with practical examples.

What Is Buffering and Why Use It?

File I/O operations interact with external storage devices, which are much slower than accessing memory. Each read or write operation that directly accesses a file can be time-consuming. Buffering introduces an intermediate memory area—called a buffer—that temporarily holds data to reduce the number of physical I/O operations.

How buffering improves efficiency:

This leads to fewer costly disk accesses and better performance.

BufferedReader Reading Text Efficiently

BufferedReader wraps a Reader (commonly FileReader) and provides methods like readLine(), which reads text line by line—a common requirement for processing text files.

Example: Reading a File Line-by-Line

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReaderExample {
    public static void main(String[] args) {
        String fileName = "example.txt";

        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            String line;
            System.out.println("File contents:");
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }
}

Explanation:

BufferedWriter Writing Text Efficiently

BufferedWriter wraps a Writer (commonly FileWriter) and buffers output, improving write efficiency and providing useful methods like newLine() to write platform-independent newline characters.

Example: Writing Lines to a File

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedWriterExample {
    public static void main(String[] args) {
        String outputFile = "output.txt";

        try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {
            writer.write("First line of text");
            writer.newLine();  // writes a newline character
            writer.write("Second line of text");
        } catch (IOException e) {
            System.err.println("Error writing file: " + e.getMessage());
        }
    }
}

Explanation:

Benefits Over Unbuffered Readers and Writers

Without buffering, every call to read() or write() might trigger a costly disk access. Buffered streams reduce this overhead by grouping operations:

Feature Unbuffered (FileReader/FileWriter) Buffered (BufferedReader/BufferedWriter)
Performance Slower, many disk accesses Faster, fewer disk accesses
Convenience Methods No readLine() or newLine() Provides useful line-oriented methods
Resource Usage Less memory used (no buffer) Uses a buffer in memory to improve speed

Best Practices When Using Buffered Streams

Combined Example: Copy a File Line-by-Line

Here’s a practical example reading from one file and writing to another using buffered streams:

import java.io.*;

public class FileCopyBuffered {
    public static void main(String[] args) {
        String sourceFile = "input.txt";
        String destinationFile = "copy.txt";

        try (BufferedReader reader = new BufferedReader(new FileReader(sourceFile));
             BufferedWriter writer = new BufferedWriter(new FileWriter(destinationFile))) {

            String line;
            while ((line = reader.readLine()) != null) {
                writer.write(line);
                writer.newLine();
            }

            System.out.println("File copied successfully.");
        } catch (IOException e) {
            System.err.println("I/O error: " + e.getMessage());
        }
    }
}

This program copies all lines from input.txt to copy.txt efficiently using buffered streams.

Summary

Understanding and using buffered streams effectively will help your Java applications handle file input and output more efficiently and reliably.

Index

11.3 Serialization and Deserialization

In Java, serialization is the process of converting an object into a sequence of bytes so it can be saved to a file, sent over a network, or stored in memory. The reverse process, deserialization, reconstructs the original object from those bytes. This mechanism allows Java programs to persist and transfer objects easily.

Why Serialization?

Sometimes, you want to store the state of an object permanently or share it between programs, possibly running on different machines. Serialization provides a standard way to:

The Serializable Interface

In Java, to make an object serializable, its class must implement the marker interface java.io.Serializable. This interface does not declare any methods; it simply signals to the Java Virtual Machine (JVM) that the class supports serialization.

Example:

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;

    // Constructor, getters, setters omitted for brevity
}

The transient Keyword

Sometimes, a field should not be serialized—for example, sensitive information like passwords or data that can be recalculated. The transient keyword marks such fields to be skipped during serialization.

private transient String password;

When deserialized, transient fields are set to their default values (null for objects, 0 for numbers, false for booleans).

Serializing and Deserializing Objects Example

Let's see a complete example that serializes a Person object to a file and then deserializes it.

import java.io.*;

public class SerializationDemo {

    public static void main(String[] args) {
        String filename = "person.ser";

        // Create a Person object
        Person person = new Person("Alice", 30);

        // Serialize the object to a file
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
            out.writeObject(person);
            System.out.println("Person object serialized.");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Deserialize the object from the file
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
            Person deserializedPerson = (Person) in.readObject();
            System.out.println("Deserialized Person: " + deserializedPerson);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class Person implements Serializable {
    private String name;
    private int age;

    // transient example: this field won't be serialized
    private transient String secretCode = "XYZ123";

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", secretCode='" + secretCode + "'}";
    }
}

Explanation:

Use Cases for Serialization

Important Considerations

Summary

Mastering serialization equips you to save and transfer complex Java objects easily, extending the power of your applications beyond runtime memory.

Index

11.4 File Paths and Directories

Managing file paths and directories is a fundamental part of file I/O in Java. Whether you’re creating folders, navigating paths, or listing directory contents, Java provides both the traditional java.io.File class and the modern, more powerful java.nio.file.Path interface with supporting classes to handle these tasks efficiently and safely.

The Legacy File Class

The File class (in java.io) represents file and directory pathnames. You can create, delete, and inspect files or directories using its methods.

Example: Creating and Deleting a Directory

import java.io.File;

public class FileExample {
    public static void main(String[] args) {
        File dir = new File("testDir");

        // Create directory if it does not exist
        if (!dir.exists()) {
            boolean created = dir.mkdir();
            System.out.println("Directory created: " + created);
        }

        // Delete directory (only if empty)
        boolean deleted = dir.delete();
        System.out.println("Directory deleted: " + deleted);
    }
}

Notes:

Listing Directory Contents

File dir = new File("someDirectory");
if (dir.isDirectory()) {
    String[] files = dir.list();
    System.out.println("Contents:");
    for (String fileName : files) {
        System.out.println(fileName);
    }
}

The Modern Path Interface and Files Utility

Since Java 7, java.nio.file.Path and the utility class java.nio.file.Files provide a more flexible way to work with file paths and operations.

Creating Paths

import java.nio.file.Path;
import java.nio.file.Paths;

Path relativePath = Paths.get("testDir");
Path absolutePath = relativePath.toAbsolutePath();

System.out.println("Relative Path: " + relativePath);
System.out.println("Absolute Path: " + absolutePath);

Creating Directories

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;

Path dirPath = Paths.get("newDir");
try {
    if (!Files.exists(dirPath)) {
        Files.createDirectory(dirPath);
        System.out.println("Directory created");
    }
} catch (IOException e) {
    System.err.println("Error creating directory: " + e.getMessage());
}

Deleting Directories

try {
    Files.deleteIfExists(dirPath);
    System.out.println("Directory deleted if it existed.");
} catch (IOException e) {
    System.err.println("Error deleting directory: " + e.getMessage());
}

Listing Directory Contents with Streams

import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;

try (DirectoryStream<Path> stream = Files.newDirectoryStream(dirPath)) {
    for (Path entry : stream) {
        System.out.println(entry.getFileName());
    }
} catch (IOException e) {
    System.err.println("Error reading directory: " + e.getMessage());
}

Relative vs Absolute Paths and Cross-Platform Best Practices

Example:

Path path = Paths.get("folder", "subfolder", "file.txt");
System.out.println(path.toString());  // Automatically uses correct separators
Click to view full runnable Code

import java.io.File;
import java.io.IOException;
import java.nio.file.*;

public class Main {
    public static void main(String[] args) {
        // Using File class to list contents
        File dir = new File("someDirectory");
        if (!dir.exists()) {
            dir.mkdir();  // Create directory if it doesn't exist
        }

        System.out.println("Listing using java.io.File:");
        if (dir.isDirectory()) {
            String[] files = dir.list();
            System.out.println("Contents:");
            for (String fileName : files) {
                System.out.println(fileName);
            }
        }

        // Using Path and Files
        Path relativePath = Paths.get("testDir");
        Path absolutePath = relativePath.toAbsolutePath();

        System.out.println("\nPath information:");
        System.out.println("Relative Path: " + relativePath);
        System.out.println("Absolute Path: " + absolutePath);

        // Create a new directory if it doesn't exist
        try {
            if (!Files.exists(relativePath)) {
                Files.createDirectory(relativePath);
                System.out.println("Directory 'testDir' created.");
            }
        } catch (IOException e) {
            System.err.println("Error creating directory: " + e.getMessage());
        }

        // List contents using DirectoryStream
        System.out.println("\nListing using java.nio.file.DirectoryStream:");
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(relativePath)) {
            for (Path entry : stream) {
                System.out.println(entry.getFileName());
            }
        } catch (IOException e) {
            System.err.println("Error reading directory: " + e.getMessage());
        }

        // Demonstrating relative vs absolute path resolution
        Path smartPath = Paths.get("folder", "subfolder", "file.txt");
        System.out.println("\nCross-platform Path: " + smartPath.toString());

        // Clean up
        try {
            Files.deleteIfExists(relativePath);
            System.out.println("Deleted 'testDir' if it existed.");
        } catch (IOException e) {
            System.err.println("Error deleting directory: " + e.getMessage());
        }
    }
}

Summary

By mastering both legacy and modern Java file path and directory tools, you’ll write code that’s both powerful and portable across platforms.

Index